ld 链接时找不到入口符号 main

ld fails to find the entry symbol main when linking

我正在使用 this article 使用内联汇编用 C 语言编写一个简单的 hello world 引导加载程序。没什么特别的,没有内核加载和其他高级主题。只是一条普通的旧 "hello world" 消息。

这是我的文件:

boot.c

/* generate 16-bit code */
__asm__(".code16\n");
/* jump boot code entry */
__asm__("jmpl [=10=]x0000, $main\n");

/* user defined function to print series of characters terminated by null 
character */
void printString(const char* pStr) {
    while (*pStr) {
      __asm__ __volatile__ (
           "int [=10=]x10" : : "a"(0x0e00 | *pStr), "b"(0x0007)
      );
      ++pStr;
    }
}

void main() {
    /* calling the printString function passing string as an argument */
    printString("Hello, world!");
}

boot.ld

ENTRY(main);
SECTIONS
{
    . = 0x7C00;
    .text : AT(0x7C00)
    {
        *(.text);
    }
    .sig : AT(0x7DFE)
    {
        SHORT(0xaa55);
    }
}

然后我 运行 以下命令:(与第一篇文章不同;改编自 another Whosebug article 因为第一篇文章中的命令对我不起作用)

gcc -std=c99 -c -g -Os -march=i686 -m32 -ffreestanding -Wall -Werror boot.c -o boot.o

ld -static -T boot.ld -m elf_i386 -nostdlib --nmagic -o boot.elf boot.o

第一行编译成功,但执行第二行时出现错误:

ld: warning: cannot find entry symbol main; defaulting to 0000000000007c00

boot.o:boot.c:(.text+0x2): undefined reference to 'main'

boot.o: In function 'main':

C:(...)/boot.c:16: undefined reference to '__main'

C:(...)/boot.c:16:(.text.startup+0xe): relocation truncated to fit: DISP16 against undefined symbol '__main'

怎么了?我使用 Windows 10 x64 和 Dev-C++ 附带的 gcc 编译器。

我建议使用 i686-elf 交叉编译器,而不是使用本机 windows 编译器和工具链。我认为您的部分问题是与 Windows i386pe 格式相关的特性。

.sig 部分可能根本没有写入,因为该未知部分可能未标记为可分配数据。结果是签名没有写入最终的二进制文件。也有可能未在 boot.ld 中设置虚拟内存地址 (VMA),因此它可能不会将引导签名推进到 512 字节扇区的最后 2 个字节。对于 Windows 格式,只读数据将放置在以 .rdata 开头的部分中。您需要确保将它们包含在数据部分之后和引导签名之前。如果不这样做,链接描述文件将默认将未处理的输入部分放在引导签名之外的末尾。

假设您已经按照评论中提到的关于额外下划线的方式进行了更改,您的文件可能会以这种方式工作:

boot.ld:

ENTRY(__main);

SECTIONS
{
    . = 0x7C00;
    .text : AT(0x7C00)
    {
        *(.text);
    }
    .data :
    {
        *(.data);
        *(.rdata*);
    }
    .sig 0x7DFE : AT(0x7DFE) SUBALIGN(0)
    {
        SHORT(0xaa55);
    }
}

compile/link 的命令和将 .sig 部分调整为常规只读分配数据部分的命令如下所示:

gcc.exe -std=c99 -c -g -Os -march=i686 -m32 -ffreestanding -Wall -Werror boot.c -o boot.o
ld.exe -mi386pe -static -T boot.ld -nostdlib --nmagic -o boot.elf boot.o

# This adjusts the .sig section attributes and updates boot.elf 
objcopy --set-section-flags .sig=alloc,contents,load,data,readonly boot.elf boot.elf
# Convert to binary
objcopy -O binary boot.elf boot.bin

其他观察结果

您对 __asm__(".code16\n"); 的使用不会为引导加载程序生成可用代码。您需要使用实验性伪 16 位代码生成,它强制汇编程序修改指令以与 32 位代码兼容,但编码后可在 16 位实模式下使用。您可以通过在每个 C/C++ 文件的顶部使用 __asm__(".code16gcc\n"); 来做到这一点。


本教程有一些不好的建议。将 JMP 执行到 main 的全局级基本汇编语句可能会重新定位到引导加载程序开头以外的其他位置(某些优化级别可能会导致这种情况)。启动代码没有将ESDSCS设置为0x0000,也没有设置SS:SP 堆栈段和指针。这可能会导致问题。


如果尝试从真实硬件上的 USB 驱动器 运行,您可能会发现您需要引导参数块。我写的这篇 讨论了这个问题以及在 真实硬件/USB/笔记本电脑问题

下可能的解决方法

注意:GCC 目前生成的唯一有用的代码是可以在 16 位实模式下 运行 的 32 位代码。这意味着您不能期望此代码在早于 386 的处理器(如 80186/80286/8086 等)上 运行

我的一般建议是不要使用 GCC 创建引导加载程序,除非您知道自己真正在做什么并且了解所涉及的所有细微差别。用汇编编写它可能是一个更好的主意。

如果您想要一个生成真正 16 位代码的 C/C++ 编译器,您可能希望查看 OpenWatcom