在编写x86汇编代码时,链接器脚本的作用是什么?

4
我对x86汇编语言非常感兴趣,想了解底层知识,于是我找到了这个极好的仓库(链接),里面有很多例子可以从EFI shell运行。

当我查看这个hello world例子时,发现有一个链接脚本,内容如下:

ENTRY(mystart)
SECTIONS
{
  . = 0x7c00;
  .text : {
    entry.o(.text)
    *(.text)
    *(.data)
    *(.rodata)
    __bss_start = .;
    /* COMMON vs BSS: https://dev59.com/qGQn5IYBdhLWcg3wcWvM */
    *(.bss)
    *(COMMON)
    __bss_end = .;
  }
  /* https://stackoverflow.com/questions/53584666/why-does-gnu-ld-include-a-section-that-does-not-appear-in-the-linker-script */
  .sig : AT(ADDR(.text) + 512 - 2)
  {
      SHORT(0xaa55);
  }
  /DISCARD/ : {
    *(.eh_frame)
  }
  __stack_bottom = .;
  . = . + 0x1000;
  __stack_top = .;
}

我无法理解为什么需要它?只是为了指定加载地址吗?我的一般理解是,当存在多个目标文件时,链接器脚本更有用,可以用来定义如何将来自多个目标文件的节合并成单个可执行文件。

如果在这个例子中没有指定链接器脚本会发生什么?(肯定至少有两个目标文件——一个来自.s,一个来自.c


你不可以尝试不指定链接脚本然后看看会发生什么吗?你可以在写完这个问题之前就完成这个尝试。 - Ken White
抱歉我的问题表述不够清晰。重点不仅在于知道立即结果,也许可以使用一些技巧/黑科技来运行它,而不需要某些链接脚本。正如标题所说,我想要理解链接脚本的作用。我已经看到了Linux内核代码,在进入C代码之前,它们从汇编语言开始。它定义了链接脚本。有没有逃脱这种情况的方法。此外,我没有使用链接脚本的先前经验。我只是读过一些理论定义和原因。 - Naveen
@old_timer - 谢谢。"从模拟器/仿真器开始...你可以更清楚地看到正在发生的事情,以解决初学者问题。"像qemu这样的仿真器是否提供任何调试优势?我认为如果出现问题,它会给我同样的黑屏幕...您能否再详细解释一下,以便我可以更多地探索并选择一个仿真器。 - Naveen
根据您选择的目标,有时我会编写自己的指令集模拟器。我记不清我检查过的PC模拟器或其他一些模拟器,当然还有Bochs等等。如果您坚持使用x86,我会从8088/8086开始,然后逐步向80386到现在发展,而不是直接跳入现在。这真的不是第一选择。PDP-11在某些方面是一个非常好的第一选择,但八进制比十六进制更有意义,但Simh已经存在并且作为一个环境运行得很好。MIPS可以使用SPIM,RISC-V有几个知名的模拟器等等。 - old_timer
应该尝试几个而不是只尝试一个。msp430似乎是pdp-11的直接后代或受其影响很大,因此具有许多良好/清洁的特性,但它们是cisc。我不会首先学习risc方面的mips,我会先选择arm thumb。在你弄清楚如何入门之后,可以购买无数个5美元和10美元的板子来玩。risc-v可能会在某些市场上取代ARM,并且这可能会很快发生,很容易编写自己的模拟器或找到一个risc-v的模拟器。但它受MIPS的影响很大,因此并不代表典型的指令集。 - old_timer
显示剩余4条评论
1个回答

5
请注意这是一个裸机示例,意味着没有操作系统。
你计算机上安装的GNU工具链很可能是为该计算机构建的,包括操作系统。
因此,当你使用"apt-get install build-essential"和"gcc hello.c -o hello"时,链接器脚本使用的是安装的工具链的一部分,并且特定于Linux,即你的发行版。(即使你从源代码编译了工具链和libc,它也会检测主机并且如果不是作为交叉编译器进行构建,则会将主机的默认库和链接器脚本作为默认值)
当你在Windows上找到并安装GNU工具链时,隐藏在该安装中的链接器脚本是特定于Windows的。
但是,当你想要将工具链用作交叉编译器时,例如针对裸机,你需要为目标环境进行链接,这通常意味着携带自己的链接器脚本,这个脚本通常过于复杂,但至少他们提供了一个。
作为x86裸机并在x86主机上进行开发,你可以(有时)使用本地编译器作为交叉编译器。同样适用于在ARM主机(例如树莓派)上构建ARM。
如果在交叉编译时没有链接器脚本,则将使用默认的链接器脚本,如果你没有为目标自定义默认的链接器脚本,则很可能会得到一个无法工作的构建。
链接器脚本的主要任务是定义链接器的地址空间。我希望在这个地址上有".text"和在另一个地址上有".data"等等。你可以在命令行中完成此操作,而无需链接器脚本,但随着越来越复杂,它变得更简单,并且GNU ld在命令行与链接器脚本之间存在一些问题(错误)。然后次要原因是对于特定语言,你需要一个引导程序,并且引导程序需要满足一些语言假设,但为了便于这样做,你需要链接器的地址空间部分来促进链接器脚本。你让链接器/工具为你完成工作。
对于C语言来说,假定.bss段的值为零,.data段将填充您在进入代码的入口点之前所要求的所有项目(通常是main()函数,但在裸机编程中您可以自己决定而且通常不使用该函数名)。为了节省工作量,您可以使用链接器将所有项放置在所需位置,因此所有文本、bss和data以及rodata等都会进行修补。它会补全函数之间的外部连接。但是现在链接器知道.bss段在哪里以及大小如何,例如,如何将这些信息传达给引导代码?gnu和其他工具链提供了一种机制(gnus解决方案不可预期地适用于其他任何解决方案。请注意,假定所有链接器脚本语言都是定制的工具链,而且不可移植,因此必须为每个工具链编写新的脚本文件和新的引导程序)可以在链接器脚本中创建变量,然后链接器就会填充您想要的内容,例如.bss段的起始地址和结束地址,或者您还可以在链接器脚本中执行更多数学运算,并获取.bss段的起始地址和大小,然后将该变量导入到引导汇编语言代码中(不能使用C,否则会导致鸡生蛋的问题),现在引导程序可以清零.bss段。 因此,我称这是引导程序代码和链接器脚本之间的结合,二者都是特定于工具链的,有多个原因:汇编语言由汇编器而不是目标指定,因此没有理由认为一个工具链的x86汇编语言(这与Intel vs AT&T无关)与另一个工具链的汇编器兼容。其次,链接器脚本语言也不能假定在工具链之间可移植,而且是特定于该工具链的。因此,对于C语言,您必须在调用任何已编译代码之前执行某些任务。组成链接和引导程序的两个或多个文件密切相关。 请注意,此示例还包括一些引导程序代码。我会寻找一个更干净的实例,实际上是真正的汇编语言而不是内联形式,尤其是因为项目中有一个汇编语言文件,C部分可能已经演示了C,而不是成为一个脚本式的内联汇编语言东西。它似乎链接到解释现象的教程,因此可能已经解释了所有这些。 裸机编程的美妙之处在于您可以做任何您想做的事情,您有更少的规则要遵循,因此作者已经这样做了。我个人不希望.bss段被清零,也不使用.data段,因此我的非可移植部分,即链接器脚本和引导程序,要简单得多。您可以拥有自己的风格和喜好,这就是裸机编程的美妙之处。

这个回答和评论正是我一直在寻找的。事实上,我还有几个问题,你神奇地预测并一次性回答了所有问题。非常感谢。 - Naveen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接