linux x_86_64下的动态链接的一点心得许多大神理解的很透彻但大都要收费。写出来与需要的朋友一起交流。从gdb调试最简单的c程序来理解。理解动态链接先写一个ex1.c最简单的c程序#includestdio.h#includestdlib.hInt 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 ex1GNU gdb (Debian 17.1-4) 17.1Copyright (C) 2025 Free Software Foundation, Inc.License GPLv3: GNU GPL version 3 or later http://gnu.org/licenses/gpl.htmlThis 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 %rbp0x000055555555514a 1: mov %rsp,%rbp 0x000055555555514d 4: lea 0xeb0(%rip),%rax # 0x5555555560040x0000555555555154 11: mov %rax,%rdi0x0000555555555157 14: call 0x555555555030 putsplt0x000055555555515c 19: mov $0x0,%edi0x0000555555555161 24: call 0x555555555040 exitpltEnd 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 putsplt ()(gdb) x/xw 0x555555555030 //这句是显示5030处的内容第二个是以十六进制显示w表示每个内存单元大小为字4字节这里没啥用0x555555555030 putsplt: 0x2fca25ff(gdb) x/i 0x0000555555555030 //显示5030处的反汇编代码这是有用的。注意看下一行jmp后面#号后面的地址。。。8000 0x555555555030 putsplt: jmp *0x2fca(%rip) # 0x555555558000 putsgot.plt(gdb) x/xw 0x555555558000 //上面一行是跳转到一个地址这个地址里因该存放着puts函数的真实地址。这一行是查看该地址的值。0x555555558000 putsgot.plt: 0x55555036 //显示8000处的值是5036上面说是puts函数的真实地址但并不是因为还没开始找现在是下一条指令的地址(gdb) si //此时继续执行下一条指令地址就是上面的50360x0000555555555036 in putsplt ()(gdb) x/i 0x555555555036 //显示5036地址的指令的反汇编代码 0x555555555036 putsplt6: 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 putsplt ()(gdb) x/i 0x555555558503b //这句是地址写错了忽略0x555555558503b: ❌ Cannot access memory at address 0x555555558503b(gdb) x/i 0x000055555555503b //显示下一句的汇编代码就跳到了所谓的桩代码研究过的都知道 0x55555555503b putsplt11: jmp 0x555555555020(gdb) si //再往下执行一步0x0000555555555020 in ?? () //这里不显示in 哪个函数了也不知道如何才能出来in正确的函数因该是编译器定好的putsplt-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_addrl_name一步步找到共享库的绝对路径。我们要得到puts函数的地址就取决于两个参数第一个reloc_arg这是取得puts这个函数的字符串第二个是共享库的绝对路径就是依靠link_map结构有了两个参数我们用dlopendlsymdlclose就能找到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.2hello world!chenmain () at ex1.c:66 exit(0);(gdb) x/xw 0x555555558000 //关键去看这个地址就是putsgot.plt的地址即puts函数的真实地址。0x555555558000 putsgot.plt:0xf7e31060// 重要这个就是puts函数的真实地址。大家看到这个地址和文章前面获得的地址0x55555036不是一个地址而是puts函数的真实地址。有了真实地址jmp这个地址就能打印helloworld了最近看到printf函数背后 的5层抽象调用vfprint等等最后调用tty驱动太神奇了不知大家理解不理解桩函数的意思我的理解大家看一下。objdump -d ex1 反汇编ex1大家找到以下代码这是上面main通过call调用printf函数。0000000000001020 putsplt-0x10:1020: ff 35 ca 2f 00 00 push 0x2fca(%rip) # 3ff01026: ff 25 cc 2f 00 00 jmp *0x2fcc(%rip) # 3ff8102c: 0f 1f 40 00 nopl 0x0(%rax)0000000000001030 putsplt:1030: ff 25 ca 2f 00 00 jmp *0x2fca(%rip) # 4000 putsGLIBC_2.2.51036: 68 00 00 00 00 push $0x0103b: e9 e0 ff ff ff jmp 1020 _init0x20我们分析putsplt下的第一句这句的意思是跳转到4000这个地址处的值这个值才是真正跳转的地址没有*是跳转到地址有*是跳转到该地址存在的数据值。在第一次跳转时4000这个地址处的数据值时1036就是jmp这个指令的下下一个地址1036.就执行下一个指令push $0x10然后执行到jmp 1020就是上面的putsplt-0x10,1020处就是压入link_map的地址1026就是跳转执行设计好的_dl_runtime_resolve函数。最终目标就是修改地址4000处当然在进程中有动态调整的值变成了真实的puts地址重点然而当其他函数再调用puts函数时也是跳转到call putsplt第一句也是jmp到4000地址处存放的数据值这里已经存放的是puts真实地址值所以就直接跳转到puts地址就不执行push $0x0和jmp 1020这些指令了也没有1020处的后续_dl_runtime_resolve函数了。至于打印以后跳转到哪里应该是main函数里的下一条指令了。