关于c:动态链接库如何函数寻址

36次阅读

共计 1786 个字符,预计需要花费 5 分钟才能阅读完成。

原文地址:动态链接库如何函数寻址

最近咱们发现 go 编译为 so 之后,内存占用涨了好多,初步剖析下来,是动静符号导致的,感觉不太合乎常识
趁着娃还在里面放假,正好学习学习~

hello world

int main() {printf("hello world\n")
}

在最简略的 hello world 中,printf 最终也是来自 libc.so 这个动态链接库
通过 objdump,咱们能够找到这一行:

400638:  call   400520 <printf@plt>

这里的 printf@plt,示意 printf 这个函数是依赖的内部函数,是要动静寻址的。

为什么须要函数寻址

不同于动态链接,函数地址在链接期(执行前),就能够确定下来了。
然而,动态链接库的地址,是在程序执行的时候,加载 so 文件时能力确定的。那么,要调用动静库中的函数,是没有方法提前晓得的地址的,所以须要一套机制来寻找函数的地址。

具体而言,分为两种寻址:

  1. so 中导出的函数地址
  2. so 外部调用非导出的函数地址

简而言之

第一种,是通过函数名来寻址的,相当于在主程序里调用了 dlsym(x, "printf") 来寻址,而后 dlsym 会在 so 文件里找 printf 的地址
第二种,是通过偏移量来寻址的,尽管相对地址不固定,然而 so 文件外部,两个函数之间的偏移量是固定的。

缓存减速

通过字符串来查找,想想也晓得是比拟低效的,那有什么方法提速呢?原理也简略,就是加缓存。
具体而言呢,是通过可执行文件中的两个段的配合,其中 .plt 可执行,.got.plt 可写,来实现缓存的成果。

还是从这一行 call 指令开始

400638:  call   400520 <printf@plt>

400520 来自 .plt 段,而且 .plt 是可执行的
持续用 objdump 能够看指令:

  400520:   jmp    QWORD PTR [rip+0x200afa]  # 601020 <printf@GLIBC_2.2.5>
  400526:   push   0x0
  40052b:   jmp    400500 <.plt>

这里有两个 jmp
第一个 jmp 的地址来自 601020,而这个 601020 来自 .got.plt 段,.got.plt 是可写的
首次执行的时候,601020 里存的就是 400526,此时意味着慢门路,须要动静查找。
当查到地址之后,会批改 601020 中的值,这样后续就能够间接一个 jmp 就实现寻址了,不须要再依照字符串查找了。

查找逻辑

至于慢门路查找,最终会调用到 _dl_lookup_symbol_x,大体而言是这么个逻辑:

  1. 先在以后可执行文件中,通过 0x0 这个偏移量,找到函数名,也就是 printf
  2. 而后再从 so 文件中,依据 printf 来查找函数地址

外围会用到两个段的数据(下面的 1 和 2 两步都会用到这两个段,只是对应来自两个不同的文件)

  1. .dynsym 用来存符号,也就是 Elf_Sym 这个构造,这个构造体里存了函数偏移地址,名称偏移地址等
  2. .dynstr 用来存字符,比方 printf 这个字符串自身就存在这里

应用 nm -D 能够看到相似这样的数据,其中 U 示意 undefined,须要内部寻址的函数

00000000004004e8 T _init
                 U printf

外部调用

这个就简略很多了,偏移量是固定的,不必动静查找,间接调用 call 指令就行了。x86 上的 call 执行也有好几个,其中有一个就是依照偏移量来的。
这里有一个有意思的小细节,比方这个例子:

000000000040061e <main>:
  40061e:       48 83 ec 08             sub    rsp,0x8
  400622:       bf 01 00 00 00          mov    edi,0x1
  400627:       e8 e6 ff ff ff          call   400612 <add>
  40062c:       89 c6                   mov    esi,eax

call 指令的跳转地址是 0x400612,这个是怎么来的呢?
e8 示意依照绝对地址寻址,而后就有了这么后果:0x40062c + 0xffffffe6 = 0x400612

平时用 objdumpgdb 看到 call 指令的地址,也都是计算后的,不留神的话,会认为都是相对地址。

总结

  1. 调用动态链接库中的函数,是通过函数名动静查找的
  2. 导出的函数,以及依赖的内部函数,都在 .dysym 里记录了元信息
  3. 函数名字符串,是存在 .dynstr 里的
  4. .plt.got.plt 这一对配合,用于寻址缓存
  5. 外部调用间接用偏移量,call 指令有一种就是依照偏移量来计算的

如果感觉有意思,欢送关注我的公众号~

正文完
 0