1、backtrace
一些内存检测工具如Valgrind,调试工具如GDB,可以查看程序运行时函数调用的堆栈信息,有时候在分析程序时要获得堆栈信息,借助于backtrace是很有帮助的,其原型如下:

#include <execinfo.h>
int backtrace(void **buffer, int size);
char **backtrace_symbols(void *const *buffer, int size);
void backtrace_symbols_fd(void *const *buffer, int size, int fd);

头文件“execinfo.h”提供了三个相关的函数,简单的说,backtrace函数用于获取堆栈的地址信息, backtrace_symbols函数把堆栈地址翻译成我们易识别的字符串, backtrace_symbols_fd函数则把字符串堆栈信息输出到文件中。

backtrace:该函数用于获取当前线程的函数调用堆栈,获取的信息将存放在buffer中,buffer是一个二级指针,可以当作指针数组来用,数组中的元素类型是void*,即从堆栈中获取的返回地址,每一个堆栈框架stack frame有一个返回地址,参数 size 用来指定buffer中可以保存void* 元素的最大值,函数返回值是buffer中实际获取的void*指针个数,最大不超过参数size的大小。

backtrace_symbols:该函数把从backtrace函数获取的信息buffer转化为一个字符串数组char**,每个字符串包含了相对于buffer中对应元素的可打印信息,包括函数名、函数的偏移地址和实际的返回地址,size指定了该数组中的元素个数,可以是backtrace函数的返回值,也可以小于这个值。需要注意的是,backtrace_symbols的返回值调用了malloc以分配存储空间,为了防止内存泄露,我们要手动调用free来释放这块内存。

backtrace_symbols_fd:该函数与backtrace_symbols 函数功能类似,不同的是,这个函数直接把结果输出到文件描述符为fd的文件中,且没有调用malloc。

在使用以上三个函数时,还需要注意一下几点:

(1)如果使用的是GCC编译链接的话,建议加上“-rdynamic”参数,这个参数的意思是告诉ELF连接器添加“-export-dynamic”标记,这样所有的符号信息symbols就会添加到动态符号表中,以便查看完整的堆栈信息。

(2)static函数不会导出符号信息symbols,在backtrace中无效。

(3)某些编译器的优化选项对获取正确的函数调用堆栈有干扰,内联函数没有堆栈框架,删除框架指针也会导致无法正确解析堆栈内容。

下面是一个简单的例子:

//backtrace_ex.cpp
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>

void my_backtrace()
{
void *buffer[100] = { NULL };
char **trace = NULL;

int size = backtrace(buffer, 100);
trace = backtrace_symbols(buffer, size);
if (NULL == trace) {
return;
}
for (int i = 0; i < size; ++i) {
printf("%s\n", trace[i]);
}
free(trace);
printf("----------done----------\n");
}

void func2()
{
my_backtrace();

}

void func()
{
func2();
}

int main()
{
func();
return 0;
}

编译执行上面的文件:

g++ backtrace_ex.cpp
./a.out
./a.out() [0x400811]
./a.out() [0x400baf]
./a.out() [0x400bba]
./a.out() [0x400bc5]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f2473cf5ec5]
./a.out() [0x400709]
----------done----------

咦!堆栈信息虽然打出来了,但是函数调用栈并不是很明确,原因是少了“-rdynamic”参数,重新编译执行如下:

g++ -rdynamic backtrace_ex.cpp
./a.out
./a.out(_Z12my_backtracev+0x44) [0x400b11]
./a.out(_Z5func2v+0x9) [0x400eaf]
./a.out(_Z4funcv+0x9) [0x400eba]
./a.out(main+0x9) [0x400ec5]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f006bdfbec5]
./a.out() [0x400a09]
----------done----------

加了“-rdynamic”参数后就很好了,我们可以看到函数名称,由于不同的平台、编译器有不同的编译规则,所以用backtrace解析出来的函数名形式是不同的,以“./a.out(_Z4funcv+0x9) [0x400eba]”为例说明,重点在于圆括号中的内容,“_Z”是个函数名开始标识符,后面的“4”表示函数名长度,接着便是真正的函数名“func”,后面的“v”表示函数参数类型为void,随后的“+0x9”是偏移地址。虽然有一定的编译规则,但可读性还不是很好,我们可以用下面介绍的方法demangle来解析这些符号。

2、demangle
demangle即符号重组,函数原型如下:

#include <cxxabi.h>
char* __cxa_demangle(const char* __mangled_name,
char* __output_buffer,
size_t* __length,
int* __status);

cxxabi.h是一个C++函数运行时库,要用g++编译链接,gcc会有问题。__mangled_name即原符号信息,是个字符串,以空字符结尾,__output_buffer用来保存符号重组后的信息,长度为__length,__status表示demangle结果,为0时表示成功,返回值指向符号重组后的字符串首地址,字符串以空字符结尾。

我们使用demangle来改进上面的例子:(把my_backtrace替换为my_backtrace2)

void my_backtrace2()
{
void *buffer[100] = { NULL };
char **trace = NULL;
int size = backtrace(buffer, 100);
trace = backtrace_symbols(buffer, size);
if (NULL == trace) {
return;
}

size_t name_size = 100;
char *name = (char*)malloc(name_size);
for (int i = 0; i < size; ++i) {
char *begin_name = 0;
char *begin_offset = 0;
char *end_offset = 0;
for (char *p = trace[i]; *p; ++p) { // 利用了符号信息的格式
if (*p == '(') { // 左括号
begin_name = p;
}
else if (*p == '+' && begin_name) { // 地址偏移符号
begin_offset = p;
}
else if (*p == ')' && begin_offset) { // 右括号
end_offset = p;
break;
}
}
if (begin_name && begin_offset && end_offset ) {
*begin_name++ = '\0';
*begin_offset++ = '\0';
*end_offset = '\0';
int status = -4; // 0 -1 -2 -3
char *ret = abi::__cxa_demangle(begin_name, name, &name_size, &status);
if (0 == status) {
name = ret;
printf("%s:%s+%s\n", trace[i], name, begin_offset);
}
else {
printf("%s:%s()+%s\n", trace[i], begin_name, begin_offset);
}
}
else {
printf("%s\n", trace[i]);
}
}
free(name);
free(trace);
printf("----------done----------\n");
}

结果如下:

g++ -rdynamic backtrace_ex.cpp
./a.out
./a.out:my_backtrace2()+0x44
./a.out:func2()+0x9
./a.out:func()+0x9
./a.out:main()+0x9
/lib/x86_64-linux-gnu/libc.so.6:__libc_start_main()+0xf5
./a.out() [0x400a09]
----------done----------

可以看出来,demangle后函数名已清晰地显示出来了,没有那些奇奇怪怪的符号了。

C语言函数调用的更多相关文章

  1. 测试c语言函数调用性能因素之测试三

    函数调用:即调用函数调用被调用函数,调用函数压栈,被调用函数执行,调用函数出栈,调用函数继续执行的一个看似简单的过程,系统底层却做了大量操作. 操作: 1,               调用函数帧指针 ...

  2. C语言函数调用过程,汇编角度查看

    C语言函数调用过程,汇编角度查看 把函数的参数按照调用约定压栈或者存储到寄存器中 调用要使用的函数,先把调用者的地址入栈,方便回来 跳转到函数 把函数使用到的一些寄存器压栈,避免修改寄存器的值 执行函 ...

  3. C语言函数调用时候内存中栈的动态变化详细分析(彩图)

    版权声明:本文为博主原创文章,未经博主允许不得转载.欢迎联系我qq2488890051 https://blog.csdn.net/kangkanglhb88008/article/details/8 ...

  4. C语言函数调用完整过程

    C语言函数调用详细过程 函数调用是步骤如下: 按照调用约定传参 调用约定是调用方(Caller)和被调方(Callee)之间按相关标准 对函数的某些行为做出是商议,其中包括下面内容: 传参顺序:是从左 ...

  5. C语言函数调用栈

    C语言函数调用栈 栈溢出(stack overflow)是最常见的二进制漏洞,在介绍栈溢出之前,我们首先需要了解函数调用栈. 函数调用栈是一块连续的用来保存函数运行状态的内存区域,调用函数(calle ...

  6. C语言函数调用约定

    在C语言中,假设我们有这样的一个函数: int function(int a,int b) 调用时只要用result = function(1,2)这样的方式就可以使用这个函数.但是,当高级语言被编译 ...

  7. C语言函数调用栈(二)

    5 函数调用约定 创建一个栈帧的最重要步骤是主调函数如何向栈中传递函数参数.主调函数必须精确存储这些参数,以便被调函数能够访问到它们.函数通过选择特定的调用约定,来表明其希望以特定方式接收参数.此外, ...

  8. 10GNU C语言函数调用

    6. C 函数调用机制概述 ​ 在 Linux 内核程序 boot/head.s 执行完基本初始化操作之后,就会跳转区执行 init/main.c 程序.那么 head.s 程序时如何把执行控制转交给 ...

  9. 关于C语言函数调用压栈和返回值问题的疑惑

    按照C编译器的约定调用函数时压栈的顺序是从右向左,并且返回值是保存在eax寄存器当中.这个命题本该是成立的,下面用一个小程序来反汇编观察执行过程: #include<stdio.h> in ...

  10. C语言函数调用栈(一)

    程序的执行过程可看作连续的函数调用.当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行.函数调用过程通常使用堆栈实现,每个用户态进程对应一个调用栈结构(call sta ...

随机推荐

  1. 简单实现Shiro单点登录(自定义Token令牌)

    1. MVC Controller 映射 sso 方法. /** * 单点登录(如已经登录,则直接跳转) * @param userCode 登录用户编码 * @param token 登录令牌,令牌 ...

  2. 【转载】C#的ArrayList使用IndexOf方法查找第一个符合条件的元素位置

    在C#的编程开发中,ArrayList集合是一个常用的非泛型类集合,在ArrayList集合中如果需要查找第一个符合条件的元素所在的位置,可以使用ArrayList集合的IndexOf方法,Index ...

  3. 串口 PLC 编程FAQ

    1. 不要频繁打开关闭串口,这是个耗时的过程,如果多个工位都争夺串口资源,则会出现卡顿,死锁. 2. PLC 的读写估计100毫秒,如果并发的写,有的写操作会失败,需要Delay或重试. 3. 通常一 ...

  4. k8s dashboard 解决secret自建证书导致浏览器访问限制

    解决参考: https://www.jianshu.com/p/c6d560d12d50   熟悉dashboard yaml文件所创建的资源 wget https://raw.githubuserc ...

  5. Fork/Join框架与Java8 Stream API 之并行流的速度比较

    Fork/Join 框架有特定的ExecutorService和线程池构成.ExecutorService可以运行任务,并且这个任务会被分解成较小的任务,它们从线程池中被fork(被不同的线程执行)出 ...

  6. Mysql数据库之备份还原(mysqldump,LVM快照,select备份,xtrabackup)

    备份类型: 热备份:读写不受影响 温备份:仅可执行读备份 冷备份:离线备份,读写均不能执行,关机备份 物理备份和逻辑备份 物理备份:复制数据文件,速度快. 逻辑备份:将数据导出之文本文件中,必要时候, ...

  7. javascript打开窗口

    项目中javascript代码,早期使用了只有ie支持的方法:Window createPopup() 方法 那个时候是2009年,而现在已经是2019-12-11了.如何改造这个早期的代码呢? 找到 ...

  8. vue - 基础(1)

    Vue基本用法 在学习Vue的基本用法之前,我们先简单的了解一些es6的语法 let: 特点:1.局部作用域 2.不会存在变量提升 3.变量不能重复声明 const: 特点:1.局部作用域 2.不会存 ...

  9. 201871010135-张玉晶《面向对象程序设计(Java)》第四周学习总结

    201871010135-张玉晶<面向对象程序设计(Java)>第四周学习总结 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这 ...

  10. Linux使用rz命令上传文件

    1.安装 yum -y install lrzsz 2.rz -be命令,选择需要上传的本地文件