【问题标题】:How to properly link 16 and 32 bit .o files?如何正确链接 16 位和 32 位 .o 文件?
【发布时间】:2021-06-28 14:16:01
【问题描述】:

我最近更换了我的计算机,从那时起,我的 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 倍。幻数已正确放置,但内核部分仍然无法使用。

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

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

【问题讨论】:

  • 谁告诉你可以cat 两个.o 文件来制作一个更大的.o 文件?你知道shell脚本中>>和>的区别吗?
  • 猫 x.o y.o >> z.o 用于我从多个开源操作系统下载的所有五个源中,我从中学习并在一些教程中使用。自从我大约两年前开始该项目以来,我也使用了它,并且直到大约一周前它都运行良好。这应该(至少在理论上)不会导致问题。
  • 好吧,我真的很怀疑将 .o 文件放在一起。对于每次构建时都会在末尾添加更多数据并且从不删除任何数据的巨型 .o 文件真的很怀疑。您是否尝试将两个 .o 文件都提供给链接器?
  • 不,我实际上并不知道它在 makefile 中的不同之处,但我只是用 MRE 进行了尝试,它产生了完全相同的输出。这是一个错误/随机吗?
  • 哦,在 mre 中,我忘记放置在构建 bin 后删除所有 .o 和 .elf 文件的部分。通常不会发生堆叠。

标签: c makefile x86 ld osdev


【解决方案1】:

确实没有 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 中,您只需将extern _main 放在程序集文件boot.asm 上,就可以引用kernel.elf 中的符号,例如_main

【讨论】:

  • 哦,-R 功能!完全忘记了。
【解决方案2】:

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

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

_start  EQU 0x7e00

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

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

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

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

【讨论】:

  • 我知道有人要求不要发表感谢评论,但非常感谢,这个答案真的很棒,比我通常得到的要好得多。我不知道为什么 -fno-pic -no-pie 不存在,但我在一些早期版本中有它,但现在在示例和原始版本中都缺少它。
【解决方案3】:

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

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

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

【讨论】:

  • 总结上面 cmets 中的讨论,如果我分别链接两个文件,然后将 elfs objcopy 到 bin,然后对 bin 进行 cat 处理,我会留下几乎 30kB 的 0x00 垃圾和0x7DFE 处的 bios 幻数。我不知道是什么原因导致了所有这些零,但输出更加混乱,因为使用 catinated o 文件,我至少还有一个可用的引导加载程序,在第二种情况下,我有 16 位和 32 位代码与大量零混合。您能否提供一个替代链接器和/或生成文件来产生我的示例的预期输出?
  • @Raphael-MdN 我可以帮助您调试此问题,但要这样做,我需要查看您用于此更新方法的确切代码和 cmets。
  • @fuz 我会相应地编辑上面的代码,谢谢^^
猜你喜欢
  • 1970-01-01
  • 2023-03-06
  • 2015-05-02
  • 1970-01-01
  • 1970-01-01
  • 2018-11-25
  • 2012-04-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多