使用 GAS AT&T 指令计算引导扇区的填充长度?

Posted

技术标签:

【中文标题】使用 GAS AT&T 指令计算引导扇区的填充长度?【英文标题】:Calculating padding length with GAS AT&T directives for a boot sector? 【发布时间】:2018-05-31 06:05:00 【问题描述】:

所以我想在引导扇区中添加填充。比方说,目前那里只有一个无限循环:jmp .。该扇区的长度必须为 512 字节。另外,需要在末尾添加魔术数字0xaa55

jmp .
.skip 508, 0
.word 0xaa55

但是,如果我想打印一些东西,但又不想计算所有字节以将其填充到正确的大小,该怎么办? 在 Intel/NASM 语法中是:

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

但是在 AT&T 语法中?那么循环(.rept)在这里不起作用,因为. 没有给出这里需要的绝对值。 .skip/.space 也有同样的问题,它们也需要一个绝对值。

有没有一种方法可以使用某种循环/.align/.skip/etc 添加填充?

编辑: 我使用as 来构建和链接ld -Ttext 0x7c00 --oformat binary,直到yasm 对于AT&T 语法足够稳定。

【问题讨论】:

只是挑剔,您的“英特尔语法”示例具体是 NASM 或 MASM 语法。 GAS .intel_syntax 仍然使用. 代替$,并且使用.word 代替dw。指令和伪指令不是英特尔在其手册中指定的语法的一部分。 假设只有一个源文件包含您的引导加载程序,您的想法将起作用。如果您尝试使用多个程序集文件制作引导加载程序并将这些文件链接在一起,则此方法将不起作用。 LD 将使用的默认链接描述文件可能会创建一个布局不正确的引导加载程序。 【参考方案1】:

使用 AT&T 语法,您可以在引导加载程序的开头放置一个标签,然后使用如下内容:

.global _start
.text
.code16
_start:
    jmp .

.space 510-(.-_start)
.word 0xaa55

期间. 是相对于当前部分开头的当前位置计数器。句点._start 之间的差异是一个绝对值,因此应该在这个表达式中起作用。

您可以使用 GCC(这将调用 LD)通过如下命令将其组装到引导加载程序中:

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

-Wl,--oformat=binary 选项将此选项传递给链接器,链接器将强制它输出到平面二进制文件。 -Wl,-Ttext=0x7c00 会将此选项传递给链接器,链接器将有效地将原点设置为 0x07c00。 -Wl,--build-id=none 告诉链接器不要使用 GCC 可能生成的构建 ID。 0x7c00 是代码预期加载的偏移量。由于我们不能使用标准库或 C 运行时,我们使用 -nostartfiles -nostdlib

排除它们

如果您打算将多个文件链接在一起,您将无法使用此方法。在这种情况下,您需要将引导签名保留在代码之外,并让链接器使用特制的链接器脚本来处理它。如果您将引导加载程序包含在单个程序集文件中,则上述方法将起作用。


我有一些通用的bootloader tips 用于编写引导加载程序代码。人们通常遇到的一个大问题是没有设置段寄存器。如果您使用 0x7c00 的原点,那么您至少需要确保 DS 寄存器我们设置为 0。如果您编写的代码使用引用标签的内存操作数,这将很重要你的代码。

使用 GNU 汇编器进行汇编时,请确保您设置了所需的正确指令编码。 .code16 将使汇编器假定目标处理器在 16 位模式下运行。 .code32 用于 32 位编码,.code64 假定为 64 位编码。 as 的默认值通常不会是 .code16


具有多个目标文件的引导加载程序

正如我上面提到的,使用多个目标文件来创建引导加载程序带来了汇编指令无法克服的挑战。为此,您可以创建一个特殊的链接器脚本,将 Origin 点设置为 0x7c00,并让链接器将引导签名放在输出文件中。使用这种方法你不需要做任何填充,链接器会为你做。下面显示了一个处理传统部分的基本链接器脚本,如.text.data.rodata。您可能永远不会使用其中的某些部分,但我将它们添加为示例:

文件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*);
    

文件boot.s 包含引导加载程序的主入口点:

# 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"

文件aux.s 有一个简单的函数来直接在屏幕上显示一个字符串:

.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

要将此引导加载程序构建到名为 boot.bin 的文件中,我们可以执行以下操作:

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

特殊的.text.bootentry 被链接描述文件放置为第一个代码。此部分应仅在一个目标文件中定义,因为它将是出现在引导加载程序开头 0x7c00 处的代码。链接描述文件将 VMA(原点)调整为 0x7dfe 并写入引导签名(0xaa55)。 0x7dfe 比前 512 个字节的末尾低 2 个字节。我们不再在汇编代码中进行任何填充,也不再在那里发出引导签名。

运行此示例引导加载程序时,应在显示屏的左上角打印一个绿色背景上黑色的字符串。

【讨论】:

干净的方法当然是使用链接器脚本将零碎放置到适当的位置,但这也应该这样做。 @fuz 你是对的,但只要有一个文件,这仍然有效。如果他为引导加载程序将对象链接在一起,他还有其他问题。我在某处有另一个关于使用 gnu 汇编器来显示此类链接器脚本(用于 LD)的引导加载程序的答案。如果 OP 正在做类似的事情。我可能对此做出了错误的假设,但 OP 并没有说明他是如何组装这个东西的。【参考方案2】:

您可以使用.org directive 非常简单地做到这一点:

    .code16
    .text
    jmp     .
    .org    510
    .word   0xaa55

.org 指令将位置计数器 (.) 推进到给定值,用零填充任何跳过的位置(默认情况下)。

请注意,位置计数器是相对于正在生成的目标文件中当前部分的开始的。这使得上面的.org 指令与执行.space 510-(.-.text) 相同,其中.text 是当前对象中.text 部分的开始,而不是最终链接的输出。这意味着它仅在从单个程序集文件创建引导加载程序时才真正起作用。

【讨论】:

以上是关于使用 GAS AT&T 指令计算引导扇区的填充长度?的主要内容,如果未能解决你的问题,请参考以下文章

将 AT&T 转换为 INTEL 语法

defuse.ca 在线 GAS 汇编器接受 movb 和 movw 的 AT&T 语法,但不接受 movl?

MBR主引导扇区解析

如何将此代码从 Intel(nasm) 转换为 AT&T(gas) 语法?

内核引导程序---head

汇编寻址方式记录