转: ltrace 是如何工作的(2016)

http://arthurchiao.art/blog/how-does-ltrace-work-zh/

strace 是一个系统调用,也是一个信号跟踪器(signal tracer),

  • 主要用于跟踪系统调用,打印系统调用的参数、返回值、时间戳等很多信息。
  • 也可以跟踪和打印进程收到的信号。
  • 在前一篇文章strace 是如何工作的 中介绍过, strace 内部基于 ptrace 系统调用。

ltrace 是一个函数库调用跟踪器(library call tracer)

  • 顾名思义,主要用于跟踪程序的函数库调用信息。
  • 它也可以像 strace 一样跟踪系统调用和信号。
  • 它的命令行参数和 strace 很相似。
  • ltrace 也是基于 ptrace。

虽然 strace 和 ltrace 底层都是基于 ptrace 系统调用, 但跟踪库函数和跟踪系统调用还是有很大差别的,这就是为什么会有 ltrace 的原因。

 

#重要概念

共享库可以被加载到任意地址。这意味着,共享库内的函数地址只有在运行时加载以后才能确定。 即使重复执行同一程序,加载同一动态库,库内的函数地址也是不同的。那么,程序是如何调用地址未知的函数的呢?

简短版的回答是:二进制格式、操作系统,以及加载器。在 Linux 上,这是一 支程序和动态加载器之间的曼妙舞蹈

下面是详细版的回答。

Linux 程序使用 ELF binary format,它提供了 许多特性。出于本文目的,我们这里只介绍两个:

  • 过程链接表(Procedure Linkage Table,PLT
  • 全局偏移表(Global Offset Table,GOT

库函数在 PLT 里都有一组对应的汇编指令,通常称作 trampoline,在函数被调用的时候执行。

PLT trampoline 代码

PLT trampoline 都遵循类似的格式,下面是一个例子:

PLT1: jmp *name1@GOTPCREL(%rip)
      pushq $index1
      jmp .PLT0
  • 第一行代码跳转到一个地址,这个地址的值存储在 GOT 中。
  • GOT 存储了绝对地址。这些地址在程序启动时初始化,指向 PLT pushq 指令所在的地址(第二行代码)。
  • 第三行 pushq $index1 为动态连接器准备一些数据,然后通过 jmp .PLT0 跳转到另一段代码,后者会进而调用动态链接器。

动态链接器通过 $index1 和其他一些数据来判断程序想调用的是哪个库函数,然后定位 到函数地址,并将其写入 GOT,覆盖之前初始化时的默认值。

当后面再次调用到这个函数时,就会直接找到函数地址,而不需再经过以上的动态链接器查 找过程。

流程总结

总结起来:

  1. 程序加载到内存时,程序和每个动态共享库(例如 DSO)通过 PLT 和 GOT 映射到内存
  2. 程序开始执行时,动态共享库里的函数的内存地址是未知的,因为动态库可以被加载到程序地址空间的任意地址
  3. 首次执行到一个函数的时候,执行过程转到函数的 PLT,里面是一些汇编代码(trampoline)
  4. trampoline 组织数据,然后调用动态链接器
  5. 动态链接器通过 PLT 准备的数据找到函数地址
  6. 将地址写入 GOT 表,然后执行转到该函数
  7. 后面再次调用到这个函数时,不再经过动态链接器,因为 GOT 里已经存储了函数地址,PLT 可以直接调用

 

posted @ 2024-03-29 17:02  skyycj  阅读(6)  评论(0编辑  收藏  举报