linux x_86_64理解动态链接
2026/7/5 14:48:01 网站建设 项目流程

linux x_86_64下的动态链接的一点心得,许多大神理解的很透彻,但大都要收费。写出来与需要的朋友一起交流。从gdb调试最简单的c程序来理解。


理解动态链接
先写一个ex1.c最简单的c程序
#include<stdio.h>
#include<stdlib.h>
Int a = 1;
Int main(){
printf("hello,world\n");
exit(0);
}


编译: gcc -g ex1.c -o ex1
特别注意:printf函数最终会会被汇编语言调用puts函数
除了gdb调试外,还需要了解gcc编译,elf文件结构,readelf、objdump、hexdump等linux命令

以下是gdb调试ex1.c程序的全过程,主要查看got.plt这个地址的值有没有变化。是gdb运行调试在屏幕上显示的截取,无删减。 //后面是自己的理解,供参考

┌──(kali㉿chen)-[~/chen]
└─$ gdb ex1
GNU gdb (Debian 17.1-4) 17.1
Copyright (C) 2025 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ex1...
(gdb) b main //main处打断点
Breakpoint 1 at 0x114d: file ex1.c, line 5.
(gdb) r //运行开始
Starting program: /home/kali/chen/ex1
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main () at ex1.c:5 //执行到main处
5 printf("hello world!chen \n"); //main下的第一个语句就是printf函数,在第五行
(gdb) disassemble main //反汇编main函数,注意看下面call语句的地址(前面的地址,就是。。。5157,每个人因机器不同不一样)
Dump of assembler code for function main:
0x0000555555555149 <+0>: push %rbp
0x000055555555514a <+1>: mov %rsp,%rbp
=> 0x000055555555514d <+4>: lea 0xeb0(%rip),%rax # 0x555555556004
0x0000555555555154 <+11>: mov %rax,%rdi
0x0000555555555157 <+14>: call 0x555555555030 <puts@plt>
0x000055555555515c <+19>: mov $0x0,%edi
0x0000555555555161 <+24>: call 0x555555555040 <exit@plt>
End of assembler dump.
(gdb) b *0x0000555555555157 //在5157处打断点
Breakpoint 2 at 0x555555555157: file ex1.c, line 5.
(gdb) c //继续执行
Continuing.

Breakpoint 2, 0x0000555555555157 in main () at ex1.c:5 //在5157处停下来
5 printf("hello world!chen \n");
(gdb) si //单步执行到printf函数里
0x0000555555555030 in puts@plt ()
(gdb) x/xw 0x555555555030 //这句是显示5030处的内容,第二个是以十六进制显示,w表示每个内存单元大小为字,4字节,这里没啥用
0x555555555030 <puts@plt>: 0x2fca25ff
(gdb) x/i 0x0000555555555030 //显示5030处的反汇编代码,这是有用的。注意看下一行jmp后面#号后面的地址。。。8000;
=> 0x555555555030 <puts@plt>: jmp *0x2fca(%rip) # 0x555555558000 <puts@got.plt>
(gdb) x/xw 0x555555558000 //上面一行是跳转到一个地址,这个地址里因该存放着puts函数的真实地址。这一行是查看该地址的值。
0x555555558000 <puts@got.plt>: 0x55555036 //显示8000处的值是5036,上面说是puts函数的真实地址,但并不是,因为还没开始找,现在是下一条指令的地址
(gdb) si //此时继续执行下一条指令,地址就是上面的5036
0x0000555555555036 in puts@plt ()
(gdb) x/i 0x555555555036 //显示5036地址的指令的反汇编代码
=> 0x555555555036 <puts@plt+6>: push $0x0 //这是压入reloc_arg,就是puts函数,这里puts是第0个,exit函数是第1个,所以压入0x0,这个有用,是puts函数载.rela.plt表中查找对应的重定位条目的依据。你们用readelf -r filename 就能看到puts在rela.plt表中是第0个项目,通过这个项目的其他信息,就能找到puts函数的的名字。特别注意,我们用readelf看到的是已经解析的表信息,我一直以为,rela.plt的第0项不已经显示puts名字了吗,怎么还要去找呢,其实在elf的二进制表中,第0个项目没有字符串,指示名字地址的偏移,要去dynstr表中才能找到。要结合hexdump -C filename(显示elf文件的十六进制码)去理解。
(gdb) si //再往下走一步
0x000055555555503b in puts@plt ()
(gdb) x/i 0x555555558503b //这句是地址写错了,忽略
0x555555558503b: ❌ Cannot access memory at address 0x555555558503b
(gdb) x/i 0x000055555555503b //显示下一句的汇编代码,就跳到了所谓的桩代码,研究过的都知道
=> 0x55555555503b <puts@plt+11>: jmp 0x555555555020
(gdb) si //再往下执行一步
0x0000555555555020 in ?? () //这里不显示in 哪个函数了,也不知道如何才能出来in正确的函数,因该是编译器定好的puts@plt-0x10函数
(gdb) x/i 0x0000555555555020 //再执行一步
=> 0x555555555020: push 0x2fca(%rip) # 0x555555557ff0 //把0x555555557ff0这个地址处的值压入堆栈,我们可以通过x/gx 0x555555557ff0显示这个地址的值,我这里显示的0x00007ffff7ffe2f0,这就是link_map结构体的起始地址。_dl_runtime_resolve通过地址找到link_map中各个成员的数据,就能找到共享库的地址,这个有点复杂,查了许多,不是要收费,就是说不清楚。我也一知半解,但现在已知道,link_map中最重要的是l_info,但在link_map结构体中没有l_info,研究好长时间,才知道l_info时链接器在过程中建立的l_info,将许多信息放入l_info[]数组中,是查找动态节,动态符号表,动态字符串表和重定位表必须的数据。那l_info依托啥来建立呢?是依托link_map结构体中l_ld(.dynamic节起始地址指针),从而结合link_map结构体中的l_addr,l_name,一步步找到共享库的绝对路径。我们要得到puts函数的地址,就取决于两个参数,第一个reloc_arg,这是取得puts这个函数的字符串,第二个是共享库的绝对路径,就是依靠link_map结构,有了两个参数,我们用dlopen,dlsym,dlclose就能找到puts函数的地址,核心是dlsym函数,jmp跳转到这个地址,就执行了puts函数。核心是dlsym函数。
(gdb) si //再执行一步
0x0000555555555026 in ?? ()
(gdb) x/i 0x0000555555555026 //跳转到_dl_runtime_resolve函数的执行地址
=> 0x555555555026: jmp *0x2fcc(%rip) # 0x555555557ff8
(gdb) si //再执行一步,就到了动态链接器。动态链接器执行_dl_runtime_resolve_fsave函数,就根据上面的两个参数,最终得出puts的地址,核心函数时_dl_fixup(),有兴趣的可以再去研究。就从如何找到puts字符串和共享库(libs.so.6)的绝对地址去入手研究,就明白了动态链接的全过程。
0x00007ffff7fda890 in ?? () from /lib64/ld-linux-x86-64.so.2
(gdb) finish //结束
Run till exit from #0 0x00007ffff7fda890 in ?? () from /lib64/ld-linux-x86-64.so.2
hello world!chen
main () at ex1.c:6
6 exit(0);
(gdb) x/xw 0x555555558000 //关键去看这个地址,就是puts@got.plt的地址,即puts函数的真实地址。
0x555555558000 <puts@got.plt>:0xf7e31060// 重要!这个就是puts函数的真实地址。大家看到,这个地址和文章前面获得的地址0x55555036不是一个地址,而是puts函数的真实地址。

有了真实地址,jmp这个地址,就能打印hello,world了

最近看到printf函数背后 的5层抽象,调用vfprint等等,最后调用tty驱动,太神奇了!

不知大家理解不理解桩函数的意思,我的理解大家看一下。

objdump -d ex1 反汇编ex1,大家找到以下代码,这是上面main通过call,调用printf函数。

0000000000001020 <puts@plt-0x10>:
1020: ff 35 ca 2f 00 00 push 0x2fca(%rip) # 3ff0
1026: ff 25 cc 2f 00 00 jmp *0x2fcc(%rip) # 3ff8
102c: 0f 1f 40 00 nopl 0x0(%rax)

0000000000001030 <puts@plt>:
1030: ff 25 ca 2f 00 00 jmp *0x2fca(%rip) # 4000 <puts@GLIBC_2.2.5>
1036: 68 00 00 00 00 push $0x0


103b: e9 e0 ff ff ff jmp 1020 <_init+0x20>


我们分析puts@plt下的第一句:这句的意思是跳转到4000这个地址处的值(这个值才是真正跳转的地址),没有*是跳转到地址,有*是跳转到该地址存在的数据值。
在第一次跳转时,4000这个地址处的数据值时1036,就是jmp这个指令的下下一个地址1036.就执行下一个指令push $0x10
然后执行到jmp 1020,就是上面的<puts@plt-0x10>,1020处就是压入link_map的地址,1026就是跳转执行设计好的_dl_runtime_resolve函数。最终目标就是修改地址4000处(当然在进程中有动态调整)的值变成了真实的puts地址
重点:然而当其他函数再调用puts函数时,也是跳转到call puts@plt,第一句也是jmp到4000地址处存放的数据值(这里已经存放的是puts真实地址值)
所以就直接跳转到puts地址,就不执行push $0x0和jmp 1020这些指令了,也没有1020处的后续_dl_runtime_resolve函数了。
至于打印以后跳转到哪里,应该是main函数里的下一条指令了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询