程序员的自我修养-第四章-静态链接
.
链接方式:相似段合并,主要分为两步:
链接方式: ld a.o b.o -e main -o ab
,
不知道为什么会一直报这个错 a.c:(.text+0x4f): undefined reference to `__stack_chk_fail’
解决方法,在编译时加上 -fno-stack-protector
标志,注意不是在链接时加
1 | ttj@ttj ~/copy [1]> gcc -c a.c -fno-stack-protector -o a.o |
目前来说,链接时设定的虚拟地址就是将来程序被加载进内存的地址,当然,内存的也是虚拟地址,不同进程之间的相同的虚拟地址可能被映射到不同的物理地址。
32 位 linux 下elf 虚拟地址分配从 0x08048000
开始,64位下从 0x400000
开始。
加载地址 与 虚拟地址 区别?一般是一样,对嵌入式系统可能不同
符号解析重定位
重定位
完成空间与地址分配后,进入了符号解析与重定位过程,这是静态链接的核心。
编译生成可重定位文件之后,会生成两类需要修正的汇编地址调用指令:绝对地址指令和相对地址指令,在链接之前,这些地址都只是一个临时的假地址,链接器在重定位阶段才会修正这些地址为真正的虚拟地址。
重定位表
通过什么判断那些地址需要被修正:重定位段(表),比如说.rel.text, .rel.data
(实际上不是根据名字来判断是不是重定位段的,是根据段的某些属性来区分的)
重定位表的每个元素代表一个需要重定位的符号,他包含两个值:第一个是地址,对于可重定位文件是相对于段的偏移,对于可执行文件可共享目标文件是虚拟地址(很多地址都是像这样的,比如说符号表的符号地址,可重定位文件也是偏移,可执行文件也是虚拟地址),第二个是重定位入口的类型和符号,注意高24位给出了表在符号表的下标哦。
符号解析
对于在重定位位表中有的符号,符号表中该符号的索引会显示UND,如果链接完所有文件后,有些未定义符号不能再全局符号表中找到,则报错。
指令修正方式
对绝对地址而言,其原来保存的值(A)一般为 0,符号的实际地址为 S,相加后也是符号的实际地址
对相对寻址而言,其原来保存的值(A):32位处理器是 -4(这个可能和指令流水线有关吧,一般RISC-V架构,五级流水,当前指令执行时,读取的地址是后面第二条指令的地址)。原图说的这么复杂,真绕,可以先看下下面的解释。因为执行当前指令时,读取的地址其实已经是当前指令的下一地址,所以只要计算符号相对于下一地址的偏移,再加上下一地址(执行阶段取值得到的地址),就会得到我们要访问的符号的虚拟地址了。关键就在于如何获得符号相对于下一地址的偏移,其实用两个地址相减就行了。假设符号的虚拟地址为符号的虚拟地址,假设下一地址为P+4(其实就是P-A嘛),相减得S-P-4(不就是S-P+A吗)。
tips. from gpt
在 x86_64 架构中,指令是按顺序取指的,并且通常是顺序执行。因此,在当前指令执行时,指令取址(IF 阶段)会指向即将执行的下一条指令。相对于当前执行的指令地址来说:
顺序执行时,取指地址偏移量为当前指令的地址,在 x86_64 中,指令长度可变,通常在 1 到 15 字节之间。
目前本电脑上计算方式有些不同,(二刷没看懂呜呜呜)
1 | //a.o |
1 | //ab |
common 块
tips.链接器不知道变量的类型,对于每个符号,他只能通过符号表判断大致的类型,比如SECTION、OBJECT、FUNC。这也导致了链接器没法判断不同文件中的变量类型是否一致,不过从GCC 10 开始,未初始化的全局变量都放到.bss段而不是common块。既然不是放到common块中,那么变量就被当做强符号看待。如下所示,按照一般理论,一个强符号一个弱符号不会报错,但这里两者的符号都存放在.bss段,被视为强符号。
C++问题
重复代码消除
比如模板、外部内联函数和虚函数表可能在不同的编译单元生成相同的代码,比如模板,一个文件使用了vector<int>
,他不知道其他文件有没有使用,就只有把所有的的代码都保留下来,这会导致:
- 空间浪费
- 指令运行效率低,不容易被Cache命中
- 地址容易出错
解决方法就是把每个模板的实例代码都放到一个段里,比如.temp.add<int>
,这样链接器就可以区分相同的实例段并处理。外部内联函数和虚函数表也是类似。
全局构造和析构
C++&ABI
经常分不清ABI、API,这里讲清楚了
好长,其他的看原文吧,p140 OneDrive
静态库链接
尝试手动实现静态链接:
调用库太多,难以一个个添加完。
链接控制过程
链接器一般有如下三种方法:
- 使用命令行给链接器指定参数。如
-e -o
- 将链接指令存放到目标文件中,较常见,visual c++ 将连接参数放在 pe 目标文件的 .drectve段来传递参数。
- 使用连接控制脚本,最灵活,最强大。
可通过 ld -verbose
查看默认链接脚本
可通过 ld -T link.script
指定链接脚本
文件如下:
1 | char* str = "Hello world!\n"; |
write 原型 int write(int filedesc, char*buffer, int size);
系统调用 通过 0x80中断实现
eax 表示 调用号,ebx,ecx,edx 用来传递参数
write 调用号为 4 ,eax = 4
filedesc 为要写入的文件句柄,向stdout输出,ebx = 0
buffer 标识要写入的缓冲区地址,要输出字符串 str, ecx = str
size 表示要写入的字节数,edx = 13
exit
exit 系统调用号为 1 eax = 1
ebx 表示进程退出码,本代码中 ebx = 42
直接编译:gcc -c -fno-builtin nomain.c -o nomain.o
出现报错,问题原因:
在64位系统下去编译32位的目标文件,这样是非法的。
解决方案:
用”-m32”强制用32位ABI去编译,即可编译通过。
1 | $gcc -c -fno-builtin -m32 TinyHelloWorld.c |
链接:ld -static -e nomain -o nomain nomain.o
- -fno-builtin GCC编译器提供了很多内置函数(Built-in Function),它会把一些常用的C库函数替换成编译器的内置函数,以达到优化的功能。比如GCC会将只有字符串参数的printf函数替换成puts,以节省格式解析的时间。exit()函数也是GCC的内置参数之一,所以我们要使用-fno-builtin参数来关闭GCC内置函数功能
- -e 是指定程序入口函数为nomain();
- -static 表示ld是静态链接的方式链接程序,而不是用默认的动态链接方式;
- -o 表示指定输出文件名为”nomain”
也会报错,
输入目标文件`TinyHelloWorld.o’是32位系统的,然而我们的平台是64位的(默认链接脚本位于/usr/lib/ldscripts下,x86_64平台默认链接64位可执行文件用的是elf_x86_64.x,链接32位可执行文件用的是elf32_x86_64.x),如果直接ld肯定不匹配,所以需要指定链接脚本与输入目标文件对应的。
解决方案:
链接的时候加上“-m elf_i386”,因为输入目标文件为i386平台。原文链接:https://blog.csdn.net/neuq_jtxw007/article/details/78112672
1 | ld -static -m elf_i386 -e nomain -o nomain nomain.o |
运行
结果如下,退出码也是正确的:
使用链接脚本
1 | ENTRY(nomain) |
似乎能正常运行:
链接后各段:
链接脚本语法
链接脚本由语句构成,分为 命令语句和赋值语句。语法与 C 语言类型,以下几点都相似:
赋值语句:略
命令语句:一般格式是由一个关键字和紧跟其后的参数所组成,如 ENTRY 命令和 SECTION 命令。一些常见命令如下表:
如下:通过 ld -verbose
显示的默认链接脚本包含以下内容,所以才可以在文件中引用
SECTION 命令:基本形式
1 | SECTIONS |
secname表示输出段的段名,secname后面必须有一个空格符,这样使得输出段名不会有歧义,后面紧跟着冒号和一对大括号。大括号里面的contents描述了一套规则和条件,它表示符合这种条件的输入段将合并到这个输出段中。
contents中可以包含若干个条件,每个条件之间以空格隔开,如果输入段符合这些条件中的任意一个即表示这个输入段符合contents规则。条件的写法如下:filename(sections), filename表示输入文件名,sections表示输入段名。让我们举几个条件的例子来看看:
- file1.o(.data) 表示输入文件中名为file1.o的文件中名叫.data的段符合条件。
- file1.o(.data .rodata) 或 file1.o(.data, .rodata) 表示输入文件中名为file1.o的文件中的名叫.data或.rodata的段符合条件。
- *(.data) 所有输入文件中的名字为.data的文件符合条件。 * 是通配符,类似于正则表达式中的 *,我们还可以使用正则表达式中的 ?、[]等规则。
- [a-z](.text*[A-Z]) 这个条件比较复杂,它表示所有输入文件中以小写字母a到z开头的文件中所有段名以.text开头,并且以大写字母A到Z结尾的段。从这个规则中你也许可以看到一些链接脚本功能的强大。很明显,当我们回头再看TinyHelloWorld.lds链接脚本,发现它的SECTIONS命令中除了有一条赋值语句之外,还有两条段规则,相信你能够很快地根据上面给出的条件做出定义分析。