如何正确 link 16 位和 32 位 .o 文件?

How to properly link 16 and 32 bit .o files?

我最近换了电脑,从那时起,我的 makefile 链吐出一个 512 字节的二进制文件,只有 0x00s 或引导加载程序,但没有其他任何东西。我将以下内容创建为 MRE:

boot.asm:

BITS 16
SECTION boot
GLOBAL _entry
EXTERN _start

_entry:
mov [disk],dl
mov ah, 0x2 ; read sectors
mov al, 6   ; amount = 6
mov ch, 0   ; zylinder = 0
mov cl, 2   ; first sector to read = 2
mov dh, 0   ; head = 0 (up)
mov dl, [disk]  ; disk
mov bx, _start  ; segment:offset address
int 0x13

cli
lgdt [GDT_POINTER]

mov eax, cr0
or al, 1
mov cr0, eax

mov ax, DATA_SEGMENT
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
jmp CODE_SEGMENT:_start

disk: DB 0x00

GDT_POINTER:
DW GDT_EXIT - GDT_ENTRY
DD GDT_ENTRY

CODE_SEGMENT EQU GDT_CODE - GDT_ENTRY
DATA_SEGMENT EQU GDT_DATA - GDT_ENTRY

GDT_ENTRY:
DQ 0x00

GDT_CODE:
DW 0xffff
DW 0x0000
DB 0x00
DB 0x9a
DB 0xcf
DB 0x00
    
GDT_DATA:
DW 0xffff
DW 0x0000
DB 0x00
DB 0x92
DB 0xcf
DB 0x00

GDT_EXIT:

TIMES 510 - ($ - $$) DB 0x00
DW 0xAA55

kernel.c:

int _main() {
    while(1) {}
}

linker16.ld:

ENTRY(_entry);
OUTPUT_FORMAT(elf32-i386);
OUTPUT_ARCH(i386);
SECTIONS
{
    . = 0x7C00;

    .text : AT(0x7C00)
    {
        *(boot)
        *(.text)
    }
    
    .data :
    {
        *(.bss);
        *(.bss*);
        *(.data);
        *(.rodata*);
        *(COMMON);
    }  
    /DISCARD/ :
    {
        *(.note*);
        *(.iplt*);
        *(.igot*);
        *(.rel*);
        *(.comment);  
    }
}

linker32.ld:

ENTRY(_main);
OUTPUT_FORMAT(elf32-i386);
OUTPUT_ARCH(i386);
SECTIONS
{
    . = 0x7E00;

    .text : AT(0x7E00)
    {
        *(.text)
    }
    
    .data :
    {
        *(.bss);
        *(.bss*);
        *(.data);
        *(.rodata*);
        *(COMMON);
    }  
    /DISCARD/ :
    {
        *(.note*);
        *(.iplt*);
        *(.igot*);
        *(.rel*);
        *(.comment);  
    }
}

生成文件:

all:
    nasm -O32 -f elf -o boot.o boot.asm
    gcc -m32 -c -g -ffreestanding -nostdlib -nostdinc -Wall -Werror -o kernel.o kernel.c
    ld -static -nostdlib -build-id=none -relocatable -T linker16.ld -o boot.elf boot.o
    ld -static -nostdlib -build-id=none -relocatable -T linker32.ld -o kernel.elf kernel.o
    objcopy -O binary boot.elf boot.bin
    objcopy -O binary kernel.elf kernel.bin
    cat boot.bin kernel.bin > sys.bin~
    rm *.o
    rm *.elf
    rm *.bin
    cat sys.bin~ > sys.bin
    rm sys.bin~
    qemu-system-i386 sys.bin
    
    
qemu:
    qemu-system-i386 sys.bin

预期的输出是一个空白屏幕,当查看兼容监视器(“信息寄存器”输出)时,GDT 在 0x7C00 之后设置了几个字节。相反,它卡在引导循环中,因为引导加载程序已正确编译,但它之后的所有内容(while 循环)都丢失了。在 .o 文件之前,一切都符合预期,但 .elf 和 .bin 太短了。有人有解决办法吗?我使用的版本是:

NASM 版本 2.14.02
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
GNU ld & objcopy(GNU Binutils for Ubuntu)2.34

编辑: 更新后的代码反而会产生一堆零,是应有大小的 60 倍。 magic number 放置正确,但内核部分仍然无法使用。

编辑 2: 我通过反复试验发现,删除链接器的 -relocatable 参数会清除大部分零,但它仍然无法按预期工作并停留在引导循环中。

编辑 3: 如果有人遇到和我一样的问题,我希望代码能够实际工作。在上面的代码中,我修复了 GDT,因为我在其中犯了一个错误。我将所有 DB 的范围缩小到 DD,但忘记了小端字节序会反转其中的所有字节,因此所有 GDT 描述符中的已用位都设置为零,从而无法进行跳转。结合fuz的回答,这个噩梦现在是可以搞定的运行

您应该 cat 16 位和 32 位二进制文​​件而不是 .o 文件。这个想法是 32 位二进制在 16 位二进制结束后立即在内存中开始;所以你安排16位二进制知道它的长度并找到32位二进制。

一种技术是从 16 位数据区域的最后一个字节开始扫描 32 位二进制文​​件的开头。 16 位预告片可能不包含 32 位 header,这在构建时是可靠的,因此您在第一次尝试启动结果时就知道该技术是否有效。

注意:虽然这个答案没有错;我怀疑 fuz 很快就会给出更好的答案。

您的程序发生了一些奇怪的事情,所以我不会尝试解决这个问题,而是继续从头开始做一些正确的事情。

您的引导加载程序基本没问题。正如您已经注意到的,您不能在引导加载程序中引用内核中的符号。默认的解决方案是跳转到内核中的一个已知位置(例如开头)并为内核安排入口点。所以我们更改 boot.asm 并删除 EXTERN _start,将其替换为

_start  EQU 0x7e00

要让内核可靠地进入 0x7e00,有一个技巧。在链接描述文件中,我们将以下行放入 linker32.ld.text 部分的开头:

.text : AT(0x7E00)
{
    _start = .;
    BYTE(0xE9);
    LONG(_main - _start - 5);

这使得 .text 以跳转到 _mainJMP 指令开始,这正是我们想要的。

接下来是随机垃圾被附加到内核的问题。这是因为你没有丢弃足够多的废话。最简单的方法是丢弃所有内容(即 *(*))并明确列出您要保留的部分。不过你需要小心;编译器可能会决定将额外的垃圾放入保持内核工作所需的奇怪部分。或者,接受编译器做任何它想做的事情并吃掉更大的内核大小。最终的链接描述文件 linker32.ld 是这样的:

OUTPUT_FORMAT(elf32-i386);
OUTPUT_ARCH(i386);
SECTIONS
{
    . = 0x7E00;

    .text : AT(0x7E00)
    {
        _start = .;
        BYTE(0xE9);
        LONG(_main - _start - 5);
        *(.text);
        *(.text.*);
    }
    
    .data :
    {
        *(.bss);
        *(.bss*);
        *(.data);
        *(.rodata*);
        *(COMMON);
    }  
    /DISCARD/ :
    {
    *(*);
    }
}

您可以在 linker16.ld 中类似地修复丢弃的部分。

接下来是构建脚本。我不会对此进行详细讨论,但您可以检查我自己所做的更改。两个重要的是 (a) 删除 -relocatable (这绝对不是你想要的)和 (b) 添加 -fno-pic -no-pie 这样编译器就不会得到任何奇怪的想法。

all:
    nasm -f elf32 boot.asm
    gcc -m32 -c -g -fno-pic -no-pie -ffreestanding -nostdlib -nostdinc -Wall -Werror -o kernel.o kernel.c
    ld -static -nostdlib -build-id=none -T linker16.ld -o boot.elf boot.o
    ld -static -nostdlib -build-id=none -T linker32.ld -o kernel.elf kernel.o
    objcopy -O binary boot.elf boot.bin
    objcopy -O binary kernel.elf kernel.bin
    cat boot.bin kernel.bin > sys.bin
    qemu-system-i386 sys.bin

qemu:
    qemu-system-i386 sys.bin

它应该像这样工作,假设引导加载程序是正确的(我在这台计算机上没有 QEMU)。

确实没有 16 位 ELF 可执行文件或对象这样的东西。 16 位代码包含在 32 位或 64 位 ELF 对象中,并且 LD 链接器支持特定于 16 位代码的重定位。

您可以生成类似 kernel.elf 的程序,并在将另一个程序链接在一起时使用这些符号。 LD 链接器具有用于此目的的选项 -R

   -R filename
   --just-symbols=filename
       Read symbol names and their addresses from filename, but do not
       relocate it or include it in the output.  This allows your output
       file to refer symbolically to absolute locations of memory defined
       in other programs. You may use this option more than once.

假设您组装 kernel.asm 并链接到 kernel.elf,您可以使用 kernel.elf 中的符号从 boot.o 构建 boot.elf,如下所示:

ld -static -nostdlib -build-id=none -T linker32.ld -o kernel.elf kernel.o
ld -static -nostdlib -build-id=none -T linker16.ld -R kernel.elf -o boot.elf boot.o

boot.asm中你可以像_main一样引用kernel.elf中的符号,只需将extern _main放在汇编文件boot.asm[=25中=]