C 语言的那些小秘密之函数的调用关系语言的那些小秘密之函数的调用关系 显示函数的调用关系是调试器的必备功能 如果我们在程序的运行中出现了崩溃的情 况 通过函数的调用关系可以快速定位问题的根源 懂得函数调用关系的实现原理也可以扩 充自己的知识面 在没有调试器的情况下 我们也可以自己来实现显示函数的调用关系 在 我们自己动手写 backtrace 函数之前 先来看看 glibc 提供的 backtrace 函数的使用 代码 如下 include include include define MAX LEVEL 4 static void call2 int i 0 void buffer MAX LEVEL 0 int size backtrace buffer MAX LEVEL for i 0 i t sh t sh rm f t sh 输出结果为 home shiyan sss c 12 home shiyan sss c 27 home shiyan sss c 34 home shiyan sss c 40 接下来看看在栈中数据的结构 函数参数的压栈是从右向左的 即先压最后一个参数 在压倒数第二个 以此类推 最后才 压入第一个参数 为了加深大家的印象 下面我给出一个测试代码 include void turn int x int y int z printf x d at X n x printf y d at X n y printf z d at X n z int main int argc char argv turn 1 2 3 return 0 运行结果为 比较打印出来的地址可以看出参数 z 的地址是最大的 x 的地址最小 参数的压栈工作完成之后 接下来就依次是 EIP EBP 临时变量的压栈操作了 最后压入 的是被调用函数本身 并为它分配临时的变量空间 而对于不同版本的 gcc 的处理方式各 有不同 老版本的 gcc 第一个临时变量放在最高的地址 第二个其次 依次顺序分布 新 版本的 gcc 则与之相反 实现 backtrace 函数的调用关系 其步骤如下 1 获取当前函数的 EBP 2 通过 EBP 获得调用者得 EIP 3 通过 EBP 获得上一级的 EBP 4 重复这个过程 知道结束 自己实现的 backtrace 函数 代码如下 include define MAX LEVEL 4 define OFFSET 4 int backtrace void buffer int size int n 0 x23f int p int i 0 int ebp p 1 OFFSET int eip p 2 OFFSET for i 0 i size i buffer i void eip p int ebp ebp p 0 eip p 1 return size static void call2 int i 0 void buffer MAX LEVEL 0 int size backtrace buffer MAX LEVEL for i 0 i t sh t sh rm f t sh root ubuntu home shiyan tt awk print addr2line 3 e tt t sh t sh rm f t sh home shiyan bac c 32 home shiyan bac c 47 home shiyan bac c 54 home shiyan bac c 60 在此重点介绍下 backtrace 函数的实现原理 通过 int p 来获取第一个临时变量的位置 因为我使用的是新版本的 gcc 有 5 个临 时变量 所以 EIP 的值存放在 p 6 中 EBP 的的值存放在 p 5 通过 buffer i void eip 可以把 eip 的强制转换为可以指向任意类型的指针 接下来通过 p int ebp 来获得上一 个函数的 ebp 获得 ebp 之后由 ebp 和 eip 的位置关系可以得到 eip 由于 ebp 指向的单元 存储的是上一个函数的 ebp 所以用一个简单的 for 循环就能实现了 另外在头文件 execinfo h 中除了声明 backtrace 函数外 还有如下两个函数也用于获取当前线 程的函数调用堆栈 char backtrace symbols void const buffer int size backtrace symbols 将从 backtrace 函数获取的信息转化为一个字符串数组 参数 buffer 应 该是从 backtrace 函数获取的数组指针 size 是该数组中的元素个数 backtrace 的返回值 函数返回值是一个指向字符串数组的指针 它的大小同 buffer 相同 每个字符串包含了一个相 对于 buffer 中对应元素的可打印信息 它包括函数名 函数的偏移地址 和实际的返回地址 现在 只有使用 ELF 二进制格式的程序和苦衷才能获取函数名称和偏移地址 在其他系统 只 有 16 进制的返回地址能被获取 另外 你可能需要传递相应的标志给链接器 以能支持函数名 功能 比如 在使用 GNU ld 的系统中 你需要传递 rdynamic 该函数的返回值是通过 malloc 函数申请的空间 因此调用这必须使用 free 函数来释放指针 注意 如果不能为字符串获取足够的空间函数的返回值将会为 NULL Function void backtrace symbols fd void const buffer int size int fd backtrace symbols fd与backtrace symbols 函数具有相同的功能 不同的是它不会给调用 者返回字符串数组 而是将结果写入文件描述符为 fd 的文件中 每个函数对应一行 它不需要 调用 malloc 函数 因此适用于有可能调用该函数会失败的情况 还是那句话 以上内容难免有误 如有错误 请纠正
展开阅读全文