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

C 如何正确链接16位和32位.o文件?,c,makefile,x86,ld,osdev,C,Makefile,X86,Ld,Osdev,我最近换了电脑,从那以后,我的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

我最近换了电脑,从那以后,我的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\u main(){
而(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
预期的输出是一个空白屏幕,当查看compat monitor(“信息寄存器”输出)时,GDT设置在0x7C00之后几个字节。相反,它被卡在一个引导循环中,因为引导加载程序被正确编译,但它之后的所有内容(while循环)都丢失了。在生成.o文件之前,一切都如预期的那样,但.elf和.bin太短。有人有解决办法吗?我使用的版本是:

NASM版本2.14.02
gcc(Ubuntu 9.3.0-17ubuntu1~20.04)9.3.0
GNULD&objcopy(Ubuntu的GNUBinutils)2.34

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

编辑2: 我通过反复试验发现,删除链接器的-relocateable参数可以清除大部分零,但它仍然不能像预期的那样工作,并停留在引导循环中

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

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

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


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

您的程序中出现了一些奇怪的东西,因此,与其尝试解决这个问题,我将继续从零开始,从正确的东西开始

您的引导加载程序基本正常。正如您已经注意到的,您不能在引导加载程序中引用内核中的符号。默认的解决方案是跳转到内核中的一个已知位置(例如开始位置),并安排一些事情使内核的入口点在那里。因此,我们更改
boot.asm
并删除
EXTERN\u start
,将其替换为

_start  EQU 0x7e00
要使内核能够可靠地在
0x7e00
处输入,有一个技巧。在链接器脚本中,我们将以下行放在
linker32.ld
.text
部分的开头:

.text : AT(0x7E00)
{
    _start = .;
    BYTE(0xE9);
    LONG(_main - _start - 5);
这使得
.text
以一条
JMP
指令开始,该指令跳转到
\u 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)删除
-可重定位的
(这绝对不是您想要的)和(b)添加
-fno pic-无饼
,这样编译器就不会有任何奇怪的想法

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

假设您组装了
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
中,您可以引用
kernel.elf
中的符号,就像
\u main
一样,只需将
extern\u main
放在汇编文件
boot.asm

上,谁告诉您可以
cat
两个.o文件来生成一个更大的.o文件?您知道shell脚本中>>和>之间的区别吗?cat x.o y.o>>z.o在我从多个开源操作系统下载的所有五个源代码中都有使用,并且在一些教程中也有使用。自从两年前我开始这个项目以来,我就一直在使用它,直到一周前,它一直工作得很好。这应该(至少在理论上)不会引起问题。好吧,我真的很怀疑将catting.o文件放在一起。而且真的很怀疑这个巨大的.o文件,它在每次构建时都会在末尾添加更多数据,而且从不删除任何数据。你试过把这两个.o文件都给链接器吗?不,我不知道它在生成文件中有什么区别,但我只是用MRE试过,结果完全一样
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