Difference between revisions of "Workshops/Assembly Snippets"

From Randomdata wiki
Jump to: navigation, search
(Snippets)
 
(Tool snippets)
Line 250: Line 250:
 
DB 0x55, 0xAA
 
DB 0x55, 0xAA
 
</pre>
 
</pre>
 +
 +
== Other tools ==
 +
Some scripts that might save you some typing:
 +
 +
'''bochsrc.txt'''
 +
<pre>
 +
floppya: 1_44="./floppy.img", status=inserted
 +
boot: floppy
 +
</pre>
 +
'''Makefile''' (Mind the TABs and put them back)
 +
<pre>
 +
%: %.asm
 +
        yasm -f bin -o floppy.img  $< -Worphan-labels -Werror
 +
        bochs -q
 +
</pre>
 +
then run <tt>make example1</tt> to assemble and run

Revision as of 11:42, 29 April 2014

Here's a small bunch of snippets that are ready for you to try as a basis

Each of these can be assembled and imaged (no copying, and you lose everything on there!) straight onto a floppy disk if you want to test it on real PC hardware. You can try the same with an USB stick but not all computers like this method.

Examples

example1.asm: just do something

Use the firmware to print an 'X' to the screen. Interrupt 0x10 handles graphics, AH=0x0e selects printing a character, AL='X' is the character to print.

BITS 16

MOV AH, 0x0e
MOV AL, 'X'
INT 0x10

JMP $

TIMES 510-($-$$) DB 0
DB 0x55, 0xAA

example2.asm: Print a string

The bios loads the first sector at 0x00007C00, so we need to inform the assembler of that so that it uses the right addresses. Segments are set to zero so that whatever number we use as an address matches the final address.

Using name anywhere results in it's address being put there during assembly. Adding [braces] around something means that that address is used to access memory. For example text returns the address of our text, [text] returns something that's stored at that address, and [SI] returns something stored at the address specified by register SI.

You can't use everything between [brackets], You can pick one from a reduced set of registers, a constant address, or the sum of these two. Registers that can hold an address are SI, DI, and BX. Using SP and BP is also possible this way, but these registers have special meanings and corresponding restrictions on their use.

BITS 16
ORG 0x7C00

    XOR AX, AX
    MOV DS, AX
    MOV ES, AX

    MOV SI, text

printloop:
    MOV AL, [SI] ; DS:SI
    CMP AL, 0
    JZ printdone
    MOV AH, 0x0e
    INT 0x10
    INC SI
    JMP printloop

printdone:

    JMP $


text: DB "Hello world!", 13, 10, 0

TIMES 510-($-$$) DB 0
DB 0x55, 0xAA


example3.asm: printing numbers

Notice SP, the stack pointer. The stack grows down, so to use the memory immediately before our code for the stack, we set SP to the end of it.

BITS 16
ORG 0x7C00

        XOR AX, AX
        MOV SS, AX
        MOV SP, 0x7C00
        MOV DS, AX
        MOV ES, AX

        MOV AX, 0x1000
        CALL printhex

        MOV AX, 0x0E20
        INT 0x10

        MOV AX, 1234
        CALL printdec

        JMP $

CALL<tt>s put the location of the next instruction on the stack before jumping, which can be used by <tt>RET to get back to where you were and implement a function call. Registers can be saved with PUSH reg and restored with POP reg. You can save all of them at once using PUSHA and POPA. Remember what gets used and what gets saved so that you don't break things of other functions.

Printing a number typically works by dividing the number by its base - 10 for decimal and 16 for hexadecimal - which leaves the digit in the remainder and a smaller number as the result of the division. Problem here is that the first digit you get is also the last digit, so here we save each of the digits and first print the smaller number - if it isn't 0 already. The function also adds the mandatory "0x" prefix that's typical of a hexidecimal number.

In decimal, multiplying a number by 10 is done by simply adding a zero, and dividing by removing a zero. Doing that with 100 adds/removes two zeroes. A computer is binary however, so adding or removing a zero only multiplies/divides by two. Printing in hexadecimal becomes easy this way by removing 4 binary digits each time (2*2*2*2 = 16). Since a computer is as fast in stripping numbers compared to performing a full division as a human, a good assembler programmer should use the opportunity when it appears.

One other new instruction is XLATB. The processor has numerous short forms for "common" memory accesses. XLATB basically performs MOV AL, [BX + AL], something that you can't do with normal instructions as neither AX or AL can otherwise be used as an address.

printhex:
        PUSHA
        MOV BX, digits
        MOV CX, AX
        MOV AX, 0x0E00 + '0'
        INT 0x10
        MOV AX, 0x0E00 + 'x'
        INT 0x10

        CALL .printdigit
        POPA
        RET

.printdigit:
        PUSH AX
        MOV AX, CX
        SHR CX, 4
        JZ .nomoredigits
        CALL .printdigit
.nomoredigits:

        AND AL, 0xF
        XLATB

        MOV AH, 0x0E
        INT 0x10

        POP AX
        RET

digits: DB "0123456789ABCDEF"

Same method for decimal, but with the now mandatory DIV. DIV takes a larger input by concatenating DX and AX, so you can divide 32-bit numbers with 16-bit code. We're not using 32-bit numbers, so DX should be zero otherwise we'll suddenly see large numbers appearing as results. DIV puts the result in AX, and the remainder in DX.

Since in ASCII, the ten digits are consecutive characters, we don't need a list to look up the proper character to print. Instead we add the character value of '0' to the result to convert the digit into it's corresponding character.

printdec:
        PUSHA
          CALL .printdigit
        POPA
        RET

.printdigit:
        XOR DX, DX
        MOV CX, 10
        DIV CX

        PUSH DX
          OR AX, AX
          JZ .nomoredigits
          CALL .printdigit
.nomoredigits:
        POP DX
        ADD DL, '0'
        MOV AL, DL
        MOV AH, 0x0E
        INT 0x10
        RET

TIMES 510 - ($-$$) DB 0
DB 0x55, 0xAA

example4.asm: keyboard input

The world is nothing without input. Therefore we use interrupt 0x16 which concerns all keyboard-related activity. setting AH=0 will enter a function that waits for a key, and then returns it - AL will have the character, and AH will have the raw key number on the keyboard. The character returned is not always a "character" though. This example simply prints whatever key you typed, so try using keys like backspace and enter and see what they do.

BITS 16

start:
        XOR AX, AX
        INT 0x16
        MOV AH, 0x0E
        INT 0x10

        JMP start


TIMES 510 - ($-$$) DB 0
DB 0x55, 0xAA


example5.asm: clearing the screen

So, you've got a screen full of garbage and you want to get rid of it. Interrupt 0x10 with AH=0 is the "set graphics mode funtion", and AL is the mode number. We're already using mode 3 in this example, but calling it anyway basically resets the mode to a clean state - and a clear screen.

BITS 16

MOV AX, 0x03
INT 0x10

MOV AH, 0x0e
MOV AL, 'X'
INT 0x10

JMP $

TIMES 510-($-$$) DB 0
DB 0x55, 0xAA

example6.asm: More than 16 bits - video hardware

While 16-bit implies 64K in addresses, Bill gates had to mention 640K. I daresay it will always be enough during this workshop, but I'd like to be proven wrong.

At any rate, we have previously been setting registers like DS, ES and SS to 0. These are called segment registers, and all memory accesses have an implicit segment register they use. Most code uses DS, but everything that involves either the stack, or SP or BP directly uses SS. Some specific instructions use ES by default, but in general ES is the most free one to use. When you write out a memory access, you can specify [ES:address] or even [SS:address] to use a different segment register for that access.

To get past 64K, the way segment registers work is by multiplying their value by 16 and adding it to the address. In this example, video hardware can be found between 0xA0000 and 0xC8000, with the particular bit we need at 0xB8000. To get there we set ES to 0xB800 (count the zeroes) and add DI. The result is an final address of ES*16 + DI. with for example ES=0xB800 and DI=0x1004 the address is 0xB9004

In our current mode, the video card stores characters with additional data in groups of two bytes. One byte is the character, the other is how it should look like. Going back to our original text printing example, we just grab the characters, add the attribute byte with increasing values and write it to the video card. Note that SI is incremented once and DI incremented twice to make the addresses match up. The final result of this is a rainbow of colours. Of course you can change this by setting AH to something perhaps less psychedelic.


BITS 16
ORG 0x7C00

    MOV AX, 0x0003
    INT 0x10

    XOR AX, AX
    MOV DS, AX
    MOV AX, 0xB800
    MOV ES, AX

    MOV SI, text
    XOR DI, DI

    XOR AX, AX

printloop:
    MOV AL, [SI] ; DS:SI
    CMP AL, 0
    JZ printdone
    INC AH
    MOV [ES:DI], AX
    INC SI
    INC DI
    INC DI
    JMP printloop

printdone:

    JMP $


text: DB 1, "-Colourful-Colouring-Colours!-", 2, 0

TIMES 510-($-$$) DB 0
DB 0x55, 0xAA

Other tools

Some scripts that might save you some typing:

bochsrc.txt

floppya: 1_44="./floppy.img", status=inserted
boot: floppy

Makefile (Mind the TABs and put them back)

%: %.asm
        yasm -f bin -o floppy.img  $< -Worphan-labels -Werror
        bochs -q

then run make example1 to assemble and run