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

Rap*_*MdN 3 c x86 makefile ld osdev

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

引导.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
Run Code Online (Sandbox Code Playgroud)

内核.c:

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
Run Code Online (Sandbox Code Playgroud)

链接器16.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);  
    }
}
Run Code Online (Sandbox Code Playgroud)

链接器32.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);  
    }
}
Run Code Online (Sandbox Code Playgroud)

生成文件:

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
Run Code Online (Sandbox Code Playgroud)

预期的输出是一个空白屏幕,当查看兼容监视器(“信息寄存器”输出)时,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:我通过反复试验发现,删除链接器的 -relocable 参数会清除大部分零,但它仍然无法按预期工作并停留在引导循环中。

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

fuz*_*fuz 5

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

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

_start  EQU 0x7e00
Run Code Online (Sandbox Code Playgroud)

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

.text : AT(0x7E00)
{
    _start = .;
    BYTE(0xE9);
    LONG(_main - _start - 5);
Run Code Online (Sandbox Code Playgroud)

这使得.text开始以JMP跳转到的指令_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/ :
    {
    *(*);
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以在 中类似地修复丢弃的部分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
Run Code Online (Sandbox Code Playgroud)

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