Search code examples
assemblyx86-16bootloadergnu-assembleratt

Calculating padding length with GAS AT&T directives for a boot sector?


So I want to add padding in the bootsector. Let's say, there is currently just an endless loop in there: jmp .. The sector needs to be 512 bytes long. Also, the magic num 0xaa55 is needed which is added at the end.

jmp .
.skip 508, 0
.word 0xaa55

But what if I want to print something but don't want to count all the bytes to pad it into the right size?
In Intel/NASM syntax would it be:

; print something
times 510-($-$$) db 0
dw 0xaa55

But in AT&T syntax? Well a loop (.rept) doesn't work here because . doesn't give an absolute value which is needed here. We have the same problem with .skip/.space, they need an absolute value too.

Is there a method to add padding using some sort of loop/.align/.skip/etc?

EDIT: I use as to build and for linking ld -Ttext 0x7c00 --oformat binary until yasm is stable enough for AT&T syntax.


Solution

  • With AT&T syntax you can put a label at the start of your bootloader and then use something like this:

    .global _start
    .text
    .code16
    _start:
        jmp .
    
    .space 510-(.-_start)
    .word 0xaa55
    

    Period . is the current location counter relative to the beginning of the current section. The difference between period . and _start is an absolute value so should work in this expression.

    You can use GCC (that will invoke LD) to assemble this to a bootloader with a command like:

    gcc -Wl,--oformat=binary -Wl,-Ttext=0x7c00 -Wl,--build-id=none \
        -nostartfiles -nostdlib -m32 -o boot.bin boot.s
    

    The option -Wl,--oformat=binary passes this option to the linker which will force it to output to a flat binary file. -Wl,-Ttext=0x7c00 will pass this option to the linker that will effectively set the origin point to 0x07c00. -Wl,--build-id=none tell the linker not to use the build id that GCC may generate. 0x7c00 is the offset the code is expected to be loaded at. Since we can't use a standard library or C runtime we exclude them with -nostartfiles -nostdlib

    You won't be able to use this method if you intend to link multiple files together. In that case you will need to leave the boot signature out of the code and let the linker take care of it with a specially crafted linker script. The method above will work if you contain your bootloader to a single assembly file.


    I have some general bootloader tips for writing bootloader code. One big issue people usually have is not setting the segment registers up. If you use an origin point of 0x7c00 then you need to make sure at a minimum that the DS register us set to 0. That will be important if you write code that uses memory operands that reference a label within your code.

    When assembling with GNU assembler ensure that you set the proper instruction encoding you want. .code16 will make the assembler assume the target processor is running in 16-bit mode. .code32 for 32-bit encoding, .code64 assumes 64-bit encoding. The default for as is generally never .code16.


    Bootloader with Multiple Object Files

    As I mentioned above using multiple object files to create your bootloader presents challenges that can't be overcome with assembly directives. In order to do this you can create a special linker script that sets the Origin point to 0x7c00 and lets the linker place the boot signature in the output file. Using this method you don't need to do any padding, the linker will do it for you. A basic linker script that deals with traditional sections like .text, .data, .rodata is shown below. You may never use some of the section, but I added them as an example:

    File bootloader.ld

    OUTPUT_FORMAT("elf32-i386");
    ENTRY(_start);
    SECTIONS
    {
        . = 0x7C00;
        /* Code section, .text.bootentry code before other code */
        .text : SUBALIGN(0) {
            *(.text.bootentry);
            *(.text)
        }
    
        /* Read only data section with no alignment */
        .rodata : SUBALIGN(0) {
            *(.rodata)
        }
    
        /* Data section with no alignment */
        .data : SUBALIGN(0) {
            *(.data)
        }
    
        /* Boot signature at 510th byte from 0x7c00 */
        .sig : AT(0x7DFE) {
            SHORT(0xaa55);
        }
    
        /DISCARD/ : {
            *(.eh_frame);
            *(.comment);
            *(.note*);
        }
    }
    

    File boot.s containing main entry point of bootloader:

    # Section .text.bootentry is always placed before all other code and data
    # in the linker script. If using multiple object files only specify
    # one .text.bootentry as that will be the code that will start executing
    # at 0x7c00
    
    .section .text.bootentry
    .code16
    .global _start
    _start:
        # Initialize the segments especially DS and set the stack to grow down from
        # start of bootloader at _start. SS:SP=0x0000:0x7c00
        xor %ax, %ax
        mov %ax, %ds
        mov %ax, %ss
        mov $_start, %sp
        cld                   # Set direction flag forward for string instructions
    
        mov  $0x20, %al       # 1st param: Attribute black on green
        xor  %cx, %cx         # 2nd param: Screen cell index to write to. (0, 0) = upper left
        mov  $boot_msg, %dx   # 3rd param: String pointer
        call print_str
    
        # Infinite loop to end bootloader
        cli
    .endloop:
        hlt
        jmp .endloop
    
    .section .rodata
    boot_msg: .asciz "My bootloader is running"
    

    File aux.s with a simple function to display a string directly to screen:

    .global print_str         # Make this available to other modules
    .section .text
    .code16
    
    # print_str (uint8_t attribute, char *str, uint16_t cellindex)
    #
    # Print a NUL terminated string directly to video memory at specified screen cell
    # using a specified attribute (foreground/background)
    #
    # Calling convention:
    #     Watcom
    # Inputs:
    #     AL = Attribute of characters to print
    #     CX = Pointer to NUL terminated string to print
    #     DX = Screen cell index to start printing at (cells are 2 bytes wide)
    # Clobbers:
    #     AX, ES
    # Returns:
    #    Nothing
    
    print_str:
        push %di
        push %si
    
        mov  $0xb800, %di     # Segment b800 = text video memory
        mov  %di, %es
        mov  %cx, %di         # DI = screen cell index (0 = upper left corner)
        mov  %dx, %si         # SI = pointer to string (2nd parameter)
        mov  %al, %ah         # AH = attribute (3rd parameter)
        jmp  .testchar
    
    # Print each character until NUL terminator found
    .nextchar:
        stosw                 # Store current attrib(AH) and char(AL) to screen
                              # Advances DI by 2. Each text mode cell is 2 bytes
    .testchar:
        lodsb                 # Load current char from string into AL(advances SI by 1)
        test %al, %al
        jne  .nextchar        # If we haven't reach NUL terminator display character
                              #     and advance to the next one
    
        pop %si
        pop %di
        ret
    

    To build this bootloader to a file called boot.bin we could do something like:

    as --32 aux.s -o aux.o
    as --32 boot.s -o boot.o
    ld -melf_i386 --oformat=binary -Tlink.ld -nostartfiles -nostdlib \
        aux.o boot.o -o boot.bin
    

    The special .text.bootentry is placed as the first code by the linker script. This section should only be defined in one object file as it will be the code that appears right at the beginning of the bootloader at 0x7c00. The linker script adjusts the VMA (origin) to 0x7dfe and writes the boot signature(0xaa55). 0x7dfe is 2 bytes below the end of the first 512 bytes. We no longer do any padding in the assembly code nor do we emit the boot signature there.

    When run this sample bootloader should print a string to the upper left of the display with black on a green background.