链接器的工作不清楚

6
我在Windows上使用C语言。这个问题之前是What happens to identifiers in a program?的一部分。我将其分开以减少问题数量。这是一个独立的查询(不依赖于先前的问题)。
如果没有任何东西需要链接(即我没有使用任何库。我知道它没有任何用处),链接器会改变汇编器的目标代码输出吗?如果是,它会改变什么?
我听说链接器还进行了一些内存映射操作。我不明白如何做到。程序没有运行,它只处于制造阶段。链接器如何映射到内存?它看起来像什么?链接器的所有功能是什么?
当人们提到“重定位”、“地址绑定”时,我真的不知道他们是什么意思。它是什么,它的目的是什么?
一些调试器显示信息,例如: 调用堆栈:0xfffef32、0xf3234fe等。这是在运行时吧?或者是所谓的链接器“内存映射”的内存地址?
当人们提到像symbolssymbol table这样的东西时,他们是否指标识符(变量名、常量名、函数名)?
我在互联网上搜索了信息,但没有找到有用的东西。也许我不知道要搜索什么。我不想读大部头的书。但如果有任何能够清晰概念的文章、教程,那也会很有帮助。
我是一名初学者程序员。所以,如果您能用简单但技术性的术语解释一下,那就太好了。

指定您使用的语言和链接器会很有帮助。不同的链接器可能会执行略微不同的操作。 - Karmic Coder
4个回答

5
当您编译一个源文件时,通常会被编译器/汇编器分成几个部分。举个假设的例子,假设使用以下部分:
- .text - 包含所有可执行代码 - .const - 包含常量数据 - .data - 包含读/写初始化数据 - .bss - 包含读/写未初始化数据
在单个源文件中,编译器/汇编器将适当的内容分配到相应的部分,并为使用的符号分配从零开始的部分偏移量。
例如:
int i;
const j = 3;
int k = 4;
int l;
int main()
{
return 1;
}

这可能会导致以下符号表:
Symbol Section Offset
i      .bss    0
j      .const  0
k      .data   0
l      .bss    4
main   .text   0

在目标文件中,除了符号表外,每个部分的数据都可以被保留。在这个例子中,.text部分将包含“return 1”的目标代码,const部分将包含3,data部分将包含4。.bss部分不需要在目标文件中,因为变量尚未初始化。
连接器可能要做的第一件事情是将输入目标文件的所有部分连接起来,并相应地调整符号偏移量。
现在我们来谈谈所谓的“重定位”或“地址绑定”。假设在一个假设的系统中,可执行代码从地址0x1000开始。假设程序的数据部分要从可执行代码之后的偶数页边界开始。连接器将把0x1000分配为连接的.text部分的基础,并调整所有符号。然后将.const、.data和.bss部分的基础类似地放置在内存中的适当位置。
有时某个部分中会有符号引用。这些引用必须由连接器更新以反映所引用符号的最终位置。目标文件可以包含看起来像“重定位记录”的内容。
section offset symbol
.text   0x1234 foo

链接器将会遍历每个节(section)中的每个偏移量,并更新那里的值以反映最终符号值。完成所有这些操作后,生成的“绝对”目标文件可以被加载到内存中(当然要在正确的位置!)并执行。

谢谢。那很有帮助。但是函数呢?它们是否具有类似的偏移量?在这种情况下,偏移量如何计算?当需要引用变量或调用函数时,它会怎么做?举个例子,如果我需要 l = k+2,它会怎么做? - Alice
我不太清楚:“假设在一个虚构的系统中,可执行代码从地址0x1000开始。”你所说的可执行代码是指什么?是.text节吗?还是另一个库对象文件的?0x1000是偏移量吗?有点困惑。我不理解这句话:“同时,假设程序的数据段要在可执行代码之后的偶数页边界开始。” - Alice
是的,在我的示例可执行代码中,它们位于.text节中。在链接过程中,它们会被连接在一起。函数的偏移量会像其他符号一样进行调整。尽管.text的偏移量可能从0x1000开始,但当所有的.text节被合并时,最终的大小可能会比这个大得多。 如果代码包含l = k+2,执行该操作的目标代码将会根据重定位记录将l和k的地址调整为最终计算出的值。 - Richard Pennington

2
如果没有任何东西要链接(即我不使用任何库。我知道它没有任何用处),连接器是否会改变汇编器的目标代码输出?如果是,它会改变什么?
它始终会链接一些初始化代码。您可以尝试编写一个空程序并将其链接,然后使用objdump -d对其进行反汇编。
我听说连接器还执行一些内存映射操作。我不明白怎么回事。程序没有运行,它只是处于制造阶段。连接器怎么能映射到内存?它会是什么样子?连接器的所有功能都有哪些?
每个系统都有一个必须遵循的可执行程序内存布局以使程序正常工作。它指定程序的不同部分放在哪里(至少是代码,初始化数据和初始化为零的数据)。连接器必须根据这些规则生成可执行文件,这些规则因系统而异,例如Windows和Linux。在嵌入式系统中,情况更有趣,程序通常位于只读存储器(Flash)中,数据位于RAM中,并且根据微控制器类型的不同,不同种类的存储器具有固定的地址范围。
当人们提到“重定位”、“地址绑定”时,我不太明白它们的含义。它是什么,目的又是什么呢?
一般来说,绑定意味着为名称赋予一个值,即将函数或全局变量的符号绑定到一个地址。
至于重定位,通常情况下,您会将多个对象文件链接在一起,每个对象文件都指定其地址作为相对于其开头的偏移量。当您将它们放在一起时,每个对象都会获得自己的地址范围,链接器通过将偏移映射到地址范围来计算符号的地址。这就是所谓的重定位。
有些调试器会显示诸如“调用堆栈:0xfffef32,0xf3234fe”等信息。这是在运行时吗?还是所谓的链接器“内存映射”的内存地址呢?
其中的0xfffef32可能是堆栈上的典型地址,因为堆栈通常位于内存顶部并向下增长。堆栈用于返回地址、局部变量和实际的函数参数。这些都是局部的,并且存储在相对于堆栈指针的地址上,因此通常不由链接器处理,而是编译器已经知道要使用哪个偏移量,并将其放入汇编代码中。
当人们提到类似符号或符号表之类的东西时,他们是指标识符(变量名、常量名、函数名)吗?
符号表是一种将符号映射到值(数字、偏移量、地址)的表格。有一些符号用于您的标识符,但也有更多用于其他用途。您的标识符可能会被修改得更多或更少以成为符号,主要是为了防止名称冲突(例如添加“_”前缀)。
链接器有一个选项--print-map来打印符号表。如果您使用gcc进行链接,则可以使用-Wl,--print-map。
如果您喜欢这种低级技术内容,您应该看看嵌入式编程,即编程微控制器,这些微控制器用于各种电子设备。对于像Windows这样的桌面系统,您通常不需要查看此类详细信息。

1

我将使用C语言进行讨论。

C程序很少不涉及至少一些库函数;因此,即使您的代码只在一个模块(文件)中,通常也会引用库函数。在编译后的程序中,这些引用位于外部引用表中,即文本名称与希望引用这些外部地址的程序位置一起出现的表格中。

链接器的工作是将您的程序与其使用的任何其他模块连接成一个单独的文件,然后将一个模块中的外部定义与另一个模块中的外部引用匹配,即通过应用程序修补所有交叉引用,以便调用命中正确的地址。

即使您没有引用任何外部模块,链接也可能需要将代码中的某些相对引用转换为绝对引用;即一旦它“知道”您的代码将要放置在文件中的位置,它就可以为事物分配正确的最终地址。


1

1
一本非常好的书 - 作者已经在网上提供了原始手稿章节,网址为http://www.iecc.com/linker/。 - anon
几次?哦天啊!难道这很困难吗? - Alice
1
@Alice 链接器在概念上非常简单,但在实践中却相当复杂。而任何好书都值得多读几遍。 - anon

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