Link 使用 GCC -m16 选项时在实模式下来自头文件的目标文件?

Link object files from header files in real mode when using GCC -m16 option?

我想在我的 C 代码中实现头文件,它部分由 GCC 16 位实模式的内联汇编代码组成,但我似乎有 link问题。这是我的头文件 console.h 的样子:

#ifndef CONSOLE_H
#define CONSOLE_H

extern void kprintf(char*);

#endif

这是 console.c:

#include "console.h"


void kprintf(char *string)
{
    for(int i=0;string[i]!='[=12=]';i++)
    {
        asm("mov [=12=]x0e,%%ah;"
            "mov [=12=]x00,%%bh;"
            "mov %0,%%al;"
            "int [=12=]x10"::"g"(string[i]):"eax", "ebx");
    }
}

最后一个hellworld.c:

asm("jmp main");
#include "console.h"

void main()
{   
    asm("mov [=13=]x1000,%ax;"
        "mov %ax,%es;"
        "mov %ax,%ds");
    char string[]="hello world";
    kprintf(string);

    asm(".rept 512;"
        "hlt;"
        ".endr");
}

我的引导加载程序在 bootloader.asm:

org 0x7c00
bits    16

section .text
mov ax,0x1000
mov ss,ax
mov sp,0x000
mov esp,0xfffe
xor ax,ax
mov es,ax 
mov ds,ax

mov [bootdrive],dl

mov bh,0
mov bp,zeichen    
mov ah,13h
mov bl,06h
mov al,1
mov cx,6
mov dh,010h
mov dl,01h
int 10h

load:
mov dl,[bootdrive]
xor ah,ah
int 13h
jc load

load2:
mov ax,0x1000
mov es,ax
xor bx,bx

mov ah,2
mov al,1
mov cx,2
xor dh,dh

mov dl,[bootdrive]
int 13h
jc load2

mov ax,0
mov es,ax
mov bh,0
mov bp,zeichen3

mov ah,13h
mov bl,06h
mov al,1
mov cx,13
mov dh,010h
mov dl,01h
int 10h

mov ax,0x1000
mov es,ax
mov ds,ax
jmp 0x1000:0x000

zeichen db  'hello2'
zeichen3 db 'soweit so gut'
bootdrive db 0
times   510 - ($-$$)    hlt
dw  0xaa55

现在我使用以下构建脚本 build.sh:

#!bin/sh

nasm -f bin bootloader.asm -o bootloader.bin
gcc hellworld.c -m16 -c -o hellworld.o -nostdlib -ffreestanding
gcc console.c -m16 -c -o console.o -nostdlib link.ld -ffreestanding
ld  -melf_i386 -Ttext=0x0000 console.o hellworld.o -o hellworld.elf
objcopy -O binary hellworld.elf hellworld.bin
cat bootloader.bin hellworld.bin >disk.img
qemu-system-i386 disk.img

和link脚本link.ld:

/*
*  link.ld
*/
OUTPUT_FORMAT(elf32-i386)
SECTIONS
{
   . = 0x0000;
   .text : { *(.startup); *(.text) }
   .data : { *(.data) }
   .bss  : { *(.bss)  }
}

不幸的是它没有工作,因为它没有打印出预期的 hello world。我认为linking命令一定有问题:

ld -melf_i386 -Ttext=0x0000 console.o hellword.o link.ld -o hellworld.elf`

如何在 16 位模式下正确 link 头文件?

当我直接在 hellworld.c 中编写 kprintf 函数时,它工作正常。我正在使用 Linux Mint Cinnamon Version 18 64 bit 进行开发。

不知道你的 bootloader.asm 做什么很难说,但是:

  1. 一定是link顺序错了;

    ld -melf_i386 -Ttext=0x0000 console.o hellworld.o -o hellworld.elf

    应该是:

    ld -melf_i386 -Ttext=0x0000 hellworld.o console.o -o hellworld.elf

    (编辑:我看到你有一个 linker 脚本可以消除重新安排的需要,但你没有将它用于 link)。

  2. 我怀疑你的引导加载程序加载了一个扇区,而你的填充:

    asm(".rept 512;"
        "hlt;"
        ".endr");
    

    ... 阻止来自其他目标文件的代码被加载,因为它填充 hellword.o 到(超过)一个扇区的大小。

问题与头文件的使用无关,这是因为你有两个编译单元成为单独的对象,并且当 linked 大于一个扇区时两者的组合大小( 512 字节)。

头文件根本不是问题所在。当您重组代码并将其拆分为多个对象时,它已确定您的构建方式以及如何将 jmp main 放入最终内核文件中的问题。

我已经创建了一个 set of files 如果您想测试整套更改以查看它们是否可以解决您的问题,可以进行下面讨论的所有调整。


尽管您展示了 linker 脚本,但您实际上并没有使用它。在你的构建文件中你有:

ld  -melf_i386 -Ttext=0x0000 console.o hellworld.o -o hellworld.elf

应该是:

ld  -melf_i386 -Tlink.ld console.o hellworld.o -o hellworld.elf

GCC 中使用 -c(编译但不编译 link)时,不要将 link.ld 指定为 link呃脚本。当您调用 LD 时,可以在 link 时指定 linker 脚本。这一行:

gcc console.c -m16 -c -o console.o -nostdlib link.ld -ffreestanding

应该是:

gcc console.c -m16 -c -o console.o -nostdlib -ffreestanding

为了让这个 linker 脚本将 jmp main 定位到输出内核文件中 第一个 的位置,您需要更改:

asm("jmp main");

收件人:

asm(".pushsection .startup\r\n"
    "jmp main\r\n"
    ".popsection\r\n");

.pushsection暂时把段改成.startup,输出指令jmp main,然后把.popsection的段恢复到原来的样子。 linker 脚本故意将任何内容放在 .startup 部分之前。这确保 jmp main (或您放置在那里的任何其他指令)显示为输出内核文件的第一条指令。 \r\n 可以替换为 ;(分号)。如果 GCC 生成程序集文件,\r\n 会产生更漂亮的输出。


正如一个现已删除的问题的评论中提到的,您的内核文件超过了单个扇区的大小。当您没有 linker 脚本时,默认脚本会将数据部分放在代码之后。您的代码重复了 hlt 指令,因此您的内核大于 1 个扇区(512 字节),并且您的引导加载程序仅读取带有 Int 13h/AH=2h 的单个扇区。

要纠正此删除:

asm(".rept 512;"
    "hlt;"
    ".endr");

并将其替换为:

asm("cli;"
    "hlt;");

您应该注意,随着内核的增长,您需要调整读入的扇区数 bootloader.asm 以确保所有内核都加载到内存中。

我还建议让 QEMU 和其他虚拟机满意,您只需生成一个众所周知的磁盘映像大小并将引导加载程序和内核放入其中。而不是:

cat bootloader.bin hellworld.bin >disk.img

使用这个:

dd if=/dev/zero of=disk.img bs=1024 count=1440
dd if=bootloader.bin of=disk.img seek=0 conv=notrunc
dd if=hellworld.bin of=disk.img seek=1 conv=notrunc

第一个命令生成一个 1440kb 的零填充文件。这正好是 1.44MB 软盘的大小。第二个命令在第一个扇区中插入 bootloader.bin 而不截断磁盘文件。第三条命令将内核文件放入从磁盘第二个扇区开始的磁盘映像中,而不截断磁盘映像。


我提供了一个稍微改进的 linker script。它被修改为删除了一些 linker 可能会插入到内核中的潜在问题,这些内容不会有太大用处,并专门标识了一些部分,如 .rodata(只读数据)等.

/*
*  link.ld
*/
OUTPUT_FORMAT(elf32-i386)
SECTIONS
{
   . = 0x0000;
   .text : { *(.startup); *(.text) }
   .data : { *(.data); *(.rodata) }
   .bss  : { *(COMMON); *(.bss) }

    /DISCARD/ : {
        *(.eh_frame);
        *(.comment);
        *(.note.gnu.build-id);
    }
}

其他评论

与您的问题无关,但可以删除此代码:

asm("mov [=21=]x1000,%ax;"
    "mov %ax,%es;"
    "mov %ax,%ds");

您在 bootloader.asm 中执行此操作,因此用相同的值再次设置这些段寄存器不会有任何用处。


您可以改进 extended assembly template by using input constraints 以通过寄存器 EAX(AX) 和 [=96 传递您需要的值=]EBX(BX) 而不是在模板内编码移动。您的代码可能看起来像:

void kprintf(const char *string)
{
    while (*string)
    {
        asm("int [=22=]x10"
        :
        :"a"((0x0e<<8) | *string++), /* AH = 0x0e, AL = char to print */
         "b"(0));                    /* BH = 0x00 page #
                                        BL = 0x00 unused in text mode */
    }
}

<<就是Cbit shift left operator. 0x0e<<8 would shift 0x0e left 8 bits which would be 0x0e00. | is bitwise OR which effectively places the character to print in the lower 8 bits. That value is then passed into the EAX register by the assembly template via input constraint"a".