linux x_86_64动态链接,gdb理解link_map参数
接着前面的动态链接文章继续聊哈。源程序ex1.c没变(参见前一次的文章)
在前面的文章中,对rela_arg还是有了些了解,但对_dl_runtime_resolve的link_map参数还没搞清楚,查了好多资料,终于对下面几个概念有了了解
1、link_map在main函数执行前就已经确定还是没确定。
答:执行前就已经确定了。
2、link_map的第一个节点是啥,第二个节点又是啥,第三个节点再是啥。。。
答:就我目前的了解,第一个节点是elf文件本身的起始地址。
第二个节点是“linux-vsdo.so.1”,虽然还不知道这个有啥作用。
第三个节点是“/usr/lib/x86)64-linux-gnu/libc.so.6“,这是共享库
第四个节点应该是/lib64/ld-linux-x86-64.so.2。这是动态链接器
上面2,3,4个节点你用ldd ex1,就清楚的显示出来了,后面我用gdb方式来理解。
你去查,问,几乎没有回答准确的,只有gdb方式一步一步调试,就明白了。
3、link_map的结构大家知道一下(可能每个glibc的版本不一样link_map的结构不一样,我的是glibc2.42)
struct link_map
{
ElfW(Addr) l_addr;//是指向加载模块的起始地址,加载libc.so.6就是libc.so.6的起始地址,加载linux-vsdo.so.1,就是这个模块的地址。
char *l_name; /*这里放着指向模块的绝对路径的字符串的地址,比较绕,这是一个地址,指向模块的名字*/
ElfW(Dyn) *l_ld; /*这是指向动态节的地址.*/
struct link_map *l_next, *l_prev; /*这是指向下一个模块的结构体和前一个模块的结构体初始地址,切记是地址*/
};
============================================
正式开始:gdb调试(我的环境是mac x86_64下使用模拟器的kali-linux6.5,属于debian-linux,下面具体的地址可能随着各位的系统不一样,具体显示的地址不一样,但思路是一样的。我也在mac m系列机器上下载了kali-linux,反汇编显示的代码和这里的也大不一样,大家仅借鉴思路,如不同,欢迎大家一起探讨)
gdb ex1 开始调试
b main //设置断点到main函数
run //运行到main处
si //进入main函数 rip=0x0000555555555154
si //rip = 0x0000555555555157 ,汇编代码call 0x555555555030<puts@plt>
si //rip= 0x0000555555555030,汇编代码jmp *0x2fca(%rip)#0x555555558000<puts@got.plt>这个地址以后存放真实的puts地址,目前是下一条指令地址
si //rip=。。。5036(前面一样,后四位写一下),汇编push $0x0, 就是rela_arg压栈
si //rip= 5036 汇编jmp 。。。5020,跳转到5020处,执行_dl_runtime_resolve定位函数,找到puts的真实地址,写到0x555555558000处。
si //rip = 5020,汇编push 0x2fca(%rip) #0x555555557ff0,将这个地址的值压栈,就是link_map的地址。
si //rip = 5026,汇编jmp *0x2fcc(%rip) #0x555555557ff8,跳转到这个地址存储的地址,执行_dl_runtime_resolve。
x/a 0x555555557ff0 // 我们先看一下,0x555555557ff0处的值是0x7ffff7ffe2f0,这就是link_map的起始地址。
x/a 0x7ffff7ffe2f0//显示的0x555555554000,这就是载入内存后地址重载的elf进程的起始地址。所以link_map的第一个节点就是elf文件载入进程后的起始地址。
si //再走一步,就会执行/lib64/ld-lijux-x86-64.so.2的_dl_runtime_resolve函数了
x/16x 0x7ffff7ffe2f0 //会显示如下
55554000 00005555f7ffe8c8 00007fff
55557de0 00005555f7ffe8d0 00007fff
.....(后面有许多)
我们看红色的,结合上面的link_map结构,红色的是l_addr,是0000555555554000,要反过来看,就是elf载入进程的起始地址。
蓝颜色的就是指向l_name的地址。这里elf进程是没有名字的,是“ ”,可以用x/s 00007ffff7ffe8c8去看一下。
紫色的是指向dynamic的地址,我们可以看到是000055555557de0,我们在elf的节头表中可以看到偏移地址是3de0,在进程中加上前面的进程起始地址555555554000,就是555555557de0,验证正确。
关键是第四个黑色的00007ffff7ffe8d0,这个就是link_map结构中的下一个模块的结构体起始地址。
x/16x 0x7ffff7ffe8d0
又出现了类似上面的一些数据
00007ffff7fc500000007ffff7fc5371
00007ffff7fc53e000007ffff7fbf170
其中7ffff7fc5371(蓝色)就是下一个模块的名字指向的地址
x/s 0x00007ffff7fc5371,见证奇迹的时候到了,显示“linux-vdso.so.1”
这就是第二个模块(或叫共享库)的名字,和ldd ex1显示的对上了。
接着就好办了,粉色的00007ffff7fbf170就是接下来一个模块的地址了,link_map结构体是一个链表模式。
x/16x 0x00007ffff7fbf170
又出现了一堆数据,
00007ffff7daf00000007ffff7fbf140
00007ffff7f95940 00007ffff7ffdda0
我们不看别的,就看红色的数据。这是第三个模块的名字的地址。
x/s 0x00007ffff7fbf140 “/usr/lib/x86_64-linux-gnu/libc.so.6"再次印证了ldd,完全正确。
==================================================
明白了l_name和l_next,其他也基本明白了,这些无聊的地址变换最终要找到属于我们的数据,在其中_dl_runtime_resolve 通过这两个参数,找到puts字符串,找到libc.so.6的完全路径,再通过dysym函数定位到puts函数真正的执行地址,就能显示了,有了真正地址,写入got.plt所在的地址,下次再调用就不用去_dl_runtime_resolve了。这就是动态链接。
这两天又问自己0x555555558000这个地址是哪里来的呢,汇编代码是
jmp *0x2fca(%rip),是当前地址的下一条地址+0x2fca,为啥呢?
其实在Elf文件的.rela.plt里,puts函数的offset是4000,载入进程后,加上其实地址0x5555555555554000,就是0x5555555555558000了,要获取puts的真实地址的地址早已确定了(有点绕哈),那么要从8000这个地址取到puts的真实地址,必须要当前地址的下一条加上某个值,才能到8000处,那就只能加0x2fca了。
那么又问自己.rela.plt里的4000又是哪里来的呢?
是gcc编译过程中产生的,其实应该是ld链接器产生的(gcc只是一个壳而已,调用了预处理,编译,汇编,链接等过程,还没学会),ld链接时根据每个节产生的数据数量,一步步产生data,dynamic,got.plt等等等等,可以看节头表,当一步步累加字节到.rela.plt节时,正好是4000(估计每个elf根据代码大小,符号多少会有不同),那就在.rela.plt的第一个条目puts处的offset,就是4000,第二个条目如exit函数,就是4008(以上是看了资料后自己猜或理解的,没有通过学习编译链接原理实践得知的)