linux函数调用栈获取

本文介绍了在Linux环境下如何通过分析函数调用栈来定位程序错误。详细解释了调用栈的布局原理及如何利用gdb工具和自定义方法获取调用栈信息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写一下关于函数调用栈的一些相关知识,对于在Linux下面进行c/c++开发,在问题定位时 查看调用栈信息是一个非常常用的定位方法,因为根据调用关系,可以知道程序的执行流程是什么样子。如果 不能查看调用栈,光知道程序在某个函数出错,还是比较难定位,假如这个函数在很多地方被调用,就很难知道是由于什么场景导致错误发生的。所以通过查看调用栈,就可以知道调用关系,当然就知道是什么场景导致问题发生。


   在gdb里面常用的命令式:bt 或全称“backtrace”就可以打印出当前函数执行的调用栈。如下面程序
   (gdb) bt
#0  0x080486da in func_3 ()
#1  0x08048766 in func_int ()
#2  0x080487ae in func_str ()
#3  0x080487ff in main ()
前面数字式层次关系,#0表示最上面,即当前函数。除了第0层前面的地址表示是当前pc值,其他地址信息都表示函数调用的返回地址,例如上面:func_int() -->func_3() ,func_3执行完成后,接着会执行0x08048766地址的指令。

上面简单介绍了一下Linux下面通过调用栈来定位问题,但调用栈的获取原理,以及如何获取,估计还是有些人会不知道的。之所以要介绍这个,因为对于一些大型系统,完善的日志功能是必不可少的,否则系统出了问题,没有相关日志,是非常痛苦的。尤其是在某些环境下,如电信领域,大多数是服务器或应用程序都是跑在单板上,出现问题了,不会像我们调试小程序那样直接用gdb进行调试。虽然某些情况下可以使用gdb attach上出问题的进程,但大多数服务器单板没有相关调试工具。所以要定位问题,基本上都是通过分析日志。还有一种情况,就是那种随机性问题,如果没有日志,那就更加痛苦了,就算你能够使用gdb也无能为力。所以日子功能是非常重要的。所以log非常重要,但是log中通常需要记录哪些信息呢?通常情况会保护函数调用出错时,把传入该函数的参数信息,或者一些关键全局变量信息,有些时候会记录日期,对于服务器程序,日期一般都会记录。另外还有一个也相对重要的就是调用栈信息。

所以下面来介绍一下获取调用栈的原理和方法:
在Linux+x86环境,c语言函数调用时,下面介绍一下c函数是怎么压栈的:栈是从高地址向下低地址移动。通常一个函数中会有参数,局部变量等相关信息,这些信息是通过下面原则分配栈的:
1、栈的信息排布为:先是局部变量存放,调用函数返回值存放,然后是调用其它函数参数函数,
1.<pre name="code" class="cpp"> 如下面程序: 
2. int B(int c, int d)  3.{ 
4.return c+d;  5.} 
6. 
7.int A(int a, int b)  8.{ 
9.int c = 0xff, d = 0xffff;  10.return B(c, d);  11.} 
12. 
13.通过objdump -d 命令可以查看反汇编指令 
14.反汇编出来后如下: 
15.00000079 <B>: 
16. 79:   55                      push   %ebp 
17. 7a:   89 e5                   mov    %esp,%ebp 
18. 7c:   8b 45 0c                mov    0xc(%ebp),%eax 
19. 7f:   03 45 08                add    0x8(%ebp),%eax 
20. 82:   5d                      pop    %ebp 
21. 83:   c3                      ret 
22. 
23.0000084 <A>: 
24. 84:   55                      push   %ebp 
25. 85:   89 e5                   mov    %esp,%ebp 
26. 87:   83 ec 18                sub    $0x18,%esp 
27. 8a:   c7 45 fc ff 00 00 00    movl   $0xff,-0x4(%ebp) 
28. 91:   c7 45 f8 ff ff 00 00    movl   $0xffff,-0x8(%ebp) 
29. 98:   8b 45 f8                mov    -0x8(%ebp),%eax 
30. 9b:   89 44 24 04             mov    %eax,0x4(%esp) 
31. 9f:   8b 45 fc                mov    -0x4(%ebp),%eax 
32. a2:   89 04 24                mov    %eax,(%esp) 
33. a5:   e8 fc ff ff ff          call   a6 <A+0x22> 
34. aa:   c9                      leave 
35. ab:   c3                      ret 
36. 
37.从上面反汇编可以看出,在A调用B时,A的调用栈布局信息如下, 
38.地址:  |---------| 
39.     |   ebp   |<--|  push   %ebp  -------------A----------------- 
40.     |---------|   | 
41.     |   c     |   |  movl   $0xff,-0x4(%ebp)   ;A函数局部变量 c 
42.     |---------|   | 
43.     |   d     |   |  movl   $0xffff,-0x8(%ebp) ;A函数局部变量 d 
44.     |---------|   | 
45.     |         |   | 
46.     |---------|   | 
47.     |         |   | 
48.     |---------|   | 
49. c+%ebp |   d     |   |  mov    %eax,0x4(%esp)    ;A调用B函数时,准备好参数d 
50.     |---------|   | 
51. 8+%ebp |   c     |   |  mov    %eax,(%esp)       ;A调用B函数时,准备好参数c 
52.     |---------|   |<----%esp      -------------A---------------- 
53. 4+%ebp | retaddr |   | A 调用B的返回地址,在执行call指令时,指令自动把call指令下一条压入这个地方。 
54.     |---------|   | 
55. %ebp-> |  ebp    |---  对应于执行B函数 :push %ebp时,把在A函数运行时的ebp保存到该位置中。 
56.     |---------| 
57.低地址: 
后面B在执行mov    0xc(%ebp),%eax时,


简单用语言描述一下函数调用过程,就那上A调用B来说,首先A函数准备好参数,即把局部变量c,d放到栈上,然后执行call B(call   a6 <A+0x22>)指令,call指令执行时默认会把当前指令的下一条指令压入栈中,然后执行B函数第一条指令即(push %ebp),所以当执行到B函数push %ebp时,栈的信息就是上面那种样子了。
 
 知道一般程序是怎么压栈的,并且A函数调用B函数会把A函数中调用B函数的那条call指令的下一条指令压栈栈中,通常情况一个函数第一条指令都是push %ebp, 功能是保存调用函数栈帧,第2条指令时mov %esp , %ebp,即把esp赋值给ebp,即初始化当前函数栈帧。
 
 在执行过程中,函数调用首先指向call执行,然后执行被调用者第一条指令(push %ebp),c语言函数调用通常都是这样情况的,而call指令又一个隐藏动作就是把下一指令(返回地址)压栈。所以在栈里面排布就是
1. --------- 
2.| ret_addr| 
3.|---------|  
4.|   ebp   |   
5.|---------|  
6.      
7.我们再看一下第二条指令,mov %esp , %ebp , 初始化当前函数栈帧。最终结果如下 
8. --------- 
9.| ret_addr|   | 
10.|---------|   | 
11.|    ebp  |---/    
12.|---------|<--| 
13.|   ...   |   | 
14.|---------|   | 
15.| ret_addr|   | 
16.|---------|   | 
17.|  ebp    |---/ 
18.|---------|<--|  
19.|  ...    |   | 
20.|---------|   |       
21.| ret_addr|   | 
22.|---------|   | 
23.|   ebp   |---/ 
24.|---------|---|  

所以我们只要知道当前%epb的值,就可以通过上面那种图示方法进行调用栈分析了。有人会问为什么libc有函数实现了,自己就没有必要了,但libc只提供获取当前线程的调用栈信息,有些时候需要获取其他线程的调用栈信息,这个时候就需要自己分析实现了,总体思路一样,只需要获取到其它线程的%ebp信息即可,但通常情况在用户态是不能够获取%ebp寄存器的,可以借助内存模块来实现。

下面写的一个小程序,一种方法使用libc库里面backtrace函数实现,还有一种就是自己通过分析调用栈信息来实现。

1.#include <stdio.h>  
2.#include <string.h>   3.#include <execinfo.h>   4. 
5./* 获取ebp寄存器值 */  6.void get_ebp(unsigned long *ebp)  7.{ 
8.        __asm__ __volatile__("mov %%ebp, %0 \r\n"  9.                 :"=m"(*ebp)  10.                 ::"memory");  11. 
12.} 
13. 
14.int my_backtrace(void **stack, int size, unsigned long ebp)  15.{ 
16.        int layer = 0;  17.    while(layer < size && ebp != 0 && *(unsigned long*)ebp != 0 && *(unsigned long *)ebp != ebp)  18.    { 
19.            stack[layer++] = *(unsigned long *)(ebp+4);  20.            ebp = *(unsigned long*)ebp;  21.    } 
22. 
23.    return layer;  24.} 
25. 
26.int func_3(int a, int b, int c)  27.{ 
28.       void *stack_addr[10];  29.       int layer;  30.       int i;  31.       char **ppstack_funcs;  32. 
33.       /* 通过调用libc函数实现 */  34.       layer = backtrace(stack_addr, 10); 
35.       ppstack_funcs = backtrace_symbols(stack_addr, layer); 
36.       for(i = 0; i < layer; i++)  37.             printf("\n%s:%p\n", ppstack_funcs[i], stack_addr[i]);  38. 
39.       /* 自己实现 */  40.       unsigned long ebp = 0;  41.       get_ebp(&ebp); 
42.       memset(stack_addr, 0, sizeof(stack_addr));  43.       layer = my_backtrace(stack_addr, 10, ebp); 
44.       for(i = 0; i < layer; i++)  45.             printf("\nmy: %p\n", stack_addr[i]);  46. 
47.     free(ppstack_funcs); 
48.     return 3;  49.} 
50. 
51.int func_int(int a, int b, int c, int d)  52.{ 
53.        int aa,bb,cc;  54.        int ret= func_3(aa,bb,cc);  55.        return (a+ b+ c+ d + ret);  56.} 
57. 
58.int func_str()  59.{ 
60.        int a = 1, b = 2;  61.        int ret;  62. 
63.        ret = func_int(a, a, b, b); 
64. 
65.        return ret;  66.} 
67. 
68.int B(int c, int d)  69.{ 
70.        return c+d;  71.} 
72. 
73.int A(int a, int b)  74.{ 
75.        int c = 0xff, d = 0xffff;  76.        return B(c, d);  77.} 
78. 
79. 
80.int main(int argc, char *argv[])  81.{ 
82.        int ret = func_str();  83.        return 0;  84.}  
 
1.程序编译加上-rdynaminc 
 
1.否则获取调用栈只有地址,没有函数名信息。 
1.运行结果:
1.<pre name="code" class="cpp">./exe() [0x80484dd]:0x80484dd 
2. 
3../exe() [0x80485ea]:0x80485ea 
4. 
5../exe() [0x8048632]:0x8048632 
6. 
7../exe() [0x8048683]:0x8048683 
8. 
9./lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0xb7dd5bd6]:0xb7dd5bd6 
10. 
11../exe() [0x8048401]:0x8048401 
12. 
13.my: 0x804858a 
14. 
15.my: 0x80485ea 
16. 
17.my: 0x8048632 
18. 
19.my: 0x8048683 
20. 
21.my: 0xb7dd5bd6 
本篇文章来源于 Linux公社网站(www.linuxidc.com)  原文链接:http://www.linuxidc.com/Linux/2011-08/41641.htm

<think>我们正在讨论Linux系统中openfile函数(通常指open()系统调用)的调用栈和实现细节。请注意,在Linux内核中,打开文件的操作主要由`do_sys_open`函数实现,而用户空间调用的`open()`函数会通过系统调用机制进入内核。###调用栈概览当用户空间程序调用`open()`时,典型的调用栈如下(以x86架构为例,基于较新内核版本如5.x):1.用户空间:调用C的`open()`函数2.C:将系统调用号(`__NR_open`)放入寄存器,并执行`syscall`指令陷入内核3.内核入口:`entry_SYSCALL_64`(x86_64架构)4.系统调用处理:`do_syscall_64`->根据系统调用号调用`__x64_sys_open`5.实际打开文件函数:`__x64_sys_open`->`__se_sys_open`->`do_sys_open`6.核心函数:`do_sys_open`负责解析路径、创建文件描述符等7.进一步调用:`do_sys_open`会调用`do_filp_open`->`path_openat`8.路径解析和文件打开:`path_openat`会处理路径查找,并最终调用具体文件系统的`open`方法###关键函数说明-`do_sys_open(longdfd,constchar__user*filename,intflags,umode_tmode)`:该函数首先分配一个未使用的文件描述符,然后通过`getname()`从用户空间获取文件名,接着调用`do_filp_open()`执行打开操作,最后安装文件描述符到当前进程的文件描述符表中。-`do_filp_open(intdfd,structfilename*pathname,conststructopen_flags*op)`:该函数创建`structfile`对象,并执行路径查找(pathlookup)以及调用具体文件系统的打开方法。-`path_openat(structnameidata*nd,conststructopen_flags*op,unsignedflags)`:这是打开文件的核心路径处理函数,它处理路径遍历(pathwalk)并最终调用`vfs_open()`。-`vfs_open(conststructpath*path,structfile*file)`:该函数调用具体文件系统的`open`方法(通过`file->f_op->open`)。例如,ext4文件系统会调用`ext4_file_open()`。###示例代码片段(内核源码片段)以下是`do_sys_open`函数的简化代码(基于Linux5.10):```clongdo_sys_open(intdfd,constchar__user*filename,intflags,umode_tmode){structopen_flagsop;intfd=build_open_flags(flags,mode,&op);structfilename*tmp;if(fd)returnfd;tmp=getname(filename);if(IS_ERR(tmp))returnPTR_ERR(tmp);fd=get_unused_fd_flags(flags);if(fd>=0){structfile*f=do_filp_open(dfd,tmp,&op);if(IS_ERR(f)){put_unused_fd(fd);fd=PTR_ERR(f);}else{fsnotify_open(f);fd_install(fd,f);}}putname(tmp);returnfd;}```###文件系统相关操作当`vfs_open()`被调用时,它会调用具体文件系统的`open`函数指针。例如,对于ext4文件系统,其文件操作结构体如下:```cconststructfile_operationsext4_file_operations={.open=ext4_file_open,.read_iter=ext4_file_read_iter,.write_iter=ext4_file_write_iter,...};```###总结Linux的open()系统调用涉及多个层次:从用户空间的函数,到系统调用入口,再到虚拟文件系统(VFS)层,最后到具体文件系统的实现。其中,VFS层提供了通用的文件操作框架,而具体文件系统则实现自己的打开方法。[^1]:代码示例参考Linux内核源码(fs/open.c)§§相关问题§§1.Linux系统调用进入内核的详细过程是怎样的?2.虚拟文件系统(VFS)在内核中扮演什么角色?3.如何跟踪一个具体的文件系统(如ext4)的open操作?4.文件描述符在内核中是如何管理的?</think>### Linux `open()` 系统调用的调用栈与实现细节 在 Linux 中,文件打开操作通过 `open()` 系统调用实现,其核心逻辑在内核中完成。以下是关键实现细节和典型调用栈(基于 Linux 5.x 内核): #### **核心调用栈** ```mermaid graph TD A[用户空间 open()] --> B[glibc 系统调用封装] B --> C[内核入口:sys_open()] C --> D[do_sys_open()] D --> E[do_filp_open()] E --> F[path_openat()] F --> G[文件系统特定 open 方法<br>如 ext4_file_open()] ``` #### **关键函数解析** 1. **`do_sys_open()`** - 分配文件描述符(`get_unused_fd_flags()`) - 解析路径(`get
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值