语言间的互操作性的实现方式:链接

2020-07-20

前言

我们常听人说:C语言、汇编语言和Rust语言是可以直接编写面向硬件的、运行在嵌入式设备上的程序甚至是操作系统的,实现这一点,用到的技术称为「链接」(Linking),链接的过程具体由称为「链接器」(linker)的程序来实现,简单来说,这个链接器程序让我们能够在C语言里面调用汇编语言代码,在汇编语言里面调C语言函数,更广泛地说,这类语言间的互操作性(Language interoperability)也不止于此——不仅限于C语言和汇编语言.C语言的编译器负责将C语言代码文本文件最终转换为「目标文件」,中间的过程大概是:C语言代码文本文件 (*.c) -> 汇编代码文本文件 (*.s) -> 目标文件 (*.o, *.so, *.dll, *.dylib),链接器(linker)的输入一般来说都是「可重定向的目标文件」(Reallocatable Object Files, 也就是其中有一些符号需要替换、一些地址需要解析和重定向的目标文件),而不是汇编代码文本文件,照这么说,汇编语言代码文本文件、C语言代码文本文件和Rust语言代码文件的地位都是相同的——因为它们各种都有能够将它们转换为目标文件的相应的程序,而假如说设定好了目标文件的输出格式,那么这三种语言的代码文本文件经过编译(Compile)和汇编(Assembly)也就得到同一种格式的目标文件(例如ELF, Mach-O, COFF等),你可以简单地理解为目标文件主要存储的是二进制(Binary)的机器码(Machine Code)及与之相关的一些元信息(Metadata),目标文件和目标文件经过链接,还是得到的目标文件,只不过这时得到的这个最终的目标文件已经没有符合需要解析和替换了并且是可执行的.

编译和链接

编译和链接

目标文件

从本质上来说,除了目标文件格式(ELF, Mach-O, COFF, COM, PE等),不同的程序设计语言编译得到的可执行文件(Executable file 或 Executable object file,目标文件的一种)没有什么不同——它们实际上就是由机器码的二进制数据组成的文件.在远古时代,人们编写程序是直接在纸带上打孔,打了孔的纸带其实就相当于现在操作系统中的可执行文件,从这个意义上说,你可以将目标文件视为现代的打孔纸带——并且它们以文件的形式存储在计算机中.ELF文件(或者说ELF格式的目标文件)与打孔纸带的共同点是都包含二进制形式的机器码,只不过ELF文件实质上存储了更多的元信息:符号和符号表、地址和偏移量,以及格式和版本等等.

举例来说,在一个待链接的ELF文件中可以包含这种含义的指令

MOV RAX, [someaddr]  ; 将起始于 someaddr 这个地址,长度为 8 个字节的数据复制到 RAX 寄存器

其中的 someaddr 具体是多少,在最终链接之前都是可以先不急着确定的,它具体指向那里需要查一查该ELF文件中的符号表(.symtab段),查了之后就知道这个 someaddr 具体是多少了,也就知道具体应该将哪个地址的 u64 复制到 RAX 里边去了,而打孔纸带相当于最终的可执行文件,待链接目标文件经过链接后变成可执行文件(可执行目标文件),相应地,文件中的内容起了变化,上面这一条指令可能就变成:

MOV RAX, [0x01234]  ; 将起始于 0x01234 这个地址,长度为 8 个字节的数据复制到 RAX 寄存器

从而就可以直接执行了,因为意义是明确的.粗略地说,这就是链接器所做的所有的工作中的一部分.

要想自己窥探各种类型的目标文件所蕴含的奥秘,可以使用 readelfobjdump 等工具,它们都已包含在最新版本的 GNU Binutils 软件包中.

参考文献

[1] Computer programming in the punched card era - Wikipedia

学习记录linking

从任意分布中构造任意分布

小小的改进给探索子博客带来了大大的进步