函数堆栈

本文通过两个C语言函数的实例,详细解析了函数在Windows平台下的汇编实现过程,重点介绍了函数堆栈指针ESP和基址指针EBP的作用及管理方式。
#include <windows.h>

int func002(char *str, int len)
{
    strcpy(str, "hello");
    
    return len;    
}

int func001(int x, char y, char *z)
{
    char buff[10];
    int aa;
    
    func002(z, aa);
    
    return 0x100;
}

void main_call()
{
    int a;
    char b;
    char c[100];
    
    func001(a, b, c);	
    
    func002(c, a);
}

void main()
{
    main_call();
}



对应的反汇编代码:cl /Fac1.asm cat.c

; Listing generated by Microsoft (R) Optimizing Compiler Version 15.00.21022.08 

	TITLE	E:\code\ctoasm\cat.c
	.686P
	.XMM
	include listing.inc
	.model	flat

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

_DATA	SEGMENT
$SG76990 DB	'hello', 00H
_DATA	ENDS
PUBLIC	_func002
EXTRN	_strcpy:PROC
; Function compile flags: /Odtp
_TEXT	SEGMENT
_str$ = 8						; size = 4
_len$ = 12						; size = 4
_func002 PROC
; File e:\code\ctoasm\cat.c
; Line 5
	push	ebp
	mov	ebp, esp
; Line 6
	push	OFFSET $SG76990
	mov	eax, DWORD PTR _str$[ebp]
	push	eax
	call	_strcpy
	add	esp, 8
; Line 8
	mov	eax, DWORD PTR _len$[ebp]   //eax才是函数返回值,len实际未赋值
; Line 9
	pop	ebp
	ret	0
_func002 ENDP
_TEXT	ENDS
PUBLIC	_func001
; Function compile flags: /Odtp
_TEXT	SEGMENT
_aa$ = -4						; size = 4
_x$ = 8							; size = 4  //0的前面压栈了pc和ebp。
_y$ = 12						; size = 1
_z$ = 16						; size = 4
_func001 PROC
; Line 12
	push	ebp
	mov	ebp, esp
	sub	esp, 20					; 00000014H
; Line 16
	mov	eax, DWORD PTR _aa$[ebp]
	push	eax
	mov	ecx, DWORD PTR _z$[ebp]
	push	ecx
	call	_func002
	add	esp, 8
; Line 18
	mov	eax, 256				; 00000100H //返回值
; Line 19
	mov	esp, ebp
	pop	ebp
	ret	0
_func001 ENDP
_TEXT	ENDS
PUBLIC	__$ArrayPad$
PUBLIC	_main_call
EXTRN	___security_cookie:DWORD
EXTRN	@__security_check_cookie@4:PROC
; Function compile flags: /Odtp
_TEXT	SEGMENT
_c$ = -120						; size = 100  //字节对齐108,并填充12字节,所以分配的局部变量空间是120B
__$ArrayPad$ = -12					; size = 4
_b$ = -5						; size = 1
_a$ = -4						; size = 4
_main_call PROC
; Line 22
	push	ebp  // 每一级的ebp指向上一级的esp起始地址。
	mov	ebp, esp
	sub	esp, 120				; 00000078H
	mov	eax, DWORD PTR ___security_cookie
	xor	eax, ebp
	mov	DWORD PTR __$ArrayPad$[ebp], eax   //&*%*&%
; Line 27
	lea	eax, DWORD PTR _c$[ebp]  //为什么不都用eax?
	push	eax
	movzx	ecx, BYTE PTR _b$[ebp]   //赋值并清零。
	push	ecx
	mov	edx, DWORD PTR _a$[ebp]
	push	edx
	call	_func001
	add	esp, 12					; 0000000cH // 调用者将压栈参数弹出堆栈。
; Line 29
	mov	eax, DWORD PTR _a$[ebp]
	push	eax
	lea	ecx, DWORD PTR _c$[ebp]
	push	ecx
	call	_func002
	add	esp, 8
; Line 30
	mov	ecx, DWORD PTR __$ArrayPad$[ebp]
	xor	ecx, ebp
	call	@__security_check_cookie@4   // 大概是堆栈检查的调试功能。
	mov	esp, ebp  // 前面将esp保存到ebp中,这里恢复esp的取值,相当于丢弃了局部变量空间。
	pop	ebp
	ret	0
_main_call ENDP
_TEXT	ENDS
PUBLIC	_main
; Function compile flags: /Odtp
_TEXT	SEGMENT
_main	PROC
; Line 33
	push	ebp
	mov	ebp, esp
; Line 34
	call	_main_call
; Line 35
	xor	eax, eax   //eax存放函数返回值
	pop	ebp
	ret	0
_main	ENDP
_TEXT	ENDS
END

汇编代码的核心是ESP、EBP的设计。

ESP是函数堆栈指针,每次PUSH减4,每次pop加4.

EBP是每个函数的基址指针,表示该函数的“起始堆栈”,可用户函数返回后恢复堆栈,类似盗梦空间的kick处理。


<think>好的,我现在需要回答用户关于C语言中如何输出函数堆栈信息的问题。首先,我得回忆一下相关知识,确保自己理解正确。用户可能想知道在程序运行时如何追踪函数的调用顺序,这在调试时非常有用。 首先,堆栈信息通常指的是函数调用堆栈,也就是当前执行点的函数调用链。例如,当函数A调用函数B,函数B又调用函数C时,堆栈会记录这个顺序。在C语言中,标准库本身并没有直接提供获取堆栈信息的函数,所以可能需要依赖特定平台或编译器的扩展。 接下来,我需要考虑不同的方法。在Linux/Unix系统中,可以使用backtrace和backtrace_symbols函数,这两个函数在execinfo.h头文件中声明。它们能获取当前的堆栈信息,但可能需要额外的步骤来将地址转换为可读的函数名,比如使用addr2line工具或者编译时加上-rdynamic选项。 另外,GCC编译器有内建函数如__builtin_return_address,可以获取调用函数的返回地址,但这需要手动处理,转换成函数名可能比较麻烦。还有像libunwind这样的第三方库,提供了更强大的堆栈展开功能,适合需要更详细信息的场景。 在Windows环境下,方法可能不同。例如,使用Win32 API中的CaptureStackBackTrace函数,或者借助Visual Studio的调试工具。这里可能需要分开说明不同平台下的实现方式。 然后,我需要考虑如何编写示例代码。比如,在Linux下使用backtrace的示例,展示如何获取堆栈的层数,然后打印出来。需要注意的是,编译时需要加上-g选项以便包含调试信息,同时可能需要-rdynamic来让符号表可用。 还要提醒用户,这些方法可能受到编译器优化影响,比如内联函数可能导致堆栈信息不准确。此外,静态函数可能无法正确显示名称,需要特别注意编译选项。 最后,应该总结各种方法的优缺点,帮助用户根据实际环境和需求选择合适的方式。同时,强调在调试时使用工具如GDB的重要性,这些工具可以更便捷地查看堆栈信息,而无需修改代码。</think>在C语言中,输出函数堆栈信息常用于调试程序崩溃或分析调用流程。以下是几种常见方法及实现步骤: --- ### 一、使用 `backtrace` 函数族(Linux/Unix) 这是Glibc提供的标准方法,适用于Linux/Unix系统。 #### 1. 示例代码 ```c #include <execinfo.h> #include <stdio.h> #include <stdlib.h> void print_stacktrace() { void *buffer[100]; int num = backtrace(buffer, 100); char **symbols = backtrace_symbols(buffer, num); if (symbols == NULL) { perror("backtrace_symbols"); return; } for (int i = 0; i < num; i++) { printf("%s\n", symbols[i]); } free(symbols); } void func_c() { print_stacktrace(); } void func_b() { func_c(); } void func_a() { func_b(); } int main() { func_a(); return 0; } ``` #### 2. 编译与运行 ```bash gcc -rdynamic -g program.c -o program ./program ``` - `-rdynamic`:导出符号表,使函数名可读 - `-g`:包含调试信息 #### 3. 输出示例 ``` ./program(func_c+0x1f) [0x55d5d5d6b1df] ./program(func_b+0x9) [0x55d5d5d6b20a] ./program(func_a+0x9) [0x55d5d5d6b21a] ./program(main+0x9) [0x55d5d5d6b22a] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xcd) [0x7f8e3b0d1d90] ./program(_start+0x2a) [0x55d5d5d6b07a] ``` --- ### 二、使用 `addr2line` 转换地址(Linux) 若需将地址转换为具体文件名和行号: ```bash addr2line -e program 0x55d5d5d6b1df ``` --- ### 三、Windows平台方法 #### 1. 使用 `CaptureStackBackTrace` ```c #include <windows.h> #include <dbghelp.h> #pragma comment(lib, "dbghelp.lib") void print_stacktrace() { void *stack[100]; WORD num = CaptureStackBackTrace(0, 100, stack, NULL); SYMBOL_INFO *symbol = (SYMBOL_INFO*)malloc(sizeof(SYMBOL_INFO) + 256); symbol->MaxNameLen = 255; symbol->SizeOfStruct = sizeof(SYMBOL_INFO); for (WORD i = 0; i < num; i++) { SymFromAddr(GetCurrentProcess(), (DWORD64)stack[i], 0, symbol); printf("%s\n", symbol->Name); } free(symbol); } ``` --- ### 四、使用GCC内建函数 ```c #include <stdio.h> void print_stack() { void *ret_addr = __builtin_return_address(0); printf("Caller address: %p\n", ret_addr); } void func() { print_stack(); } int main() { func(); } ``` --- ### 五、注意事项 1. **编译器优化影响**:内联函数(`inline`)或尾递归优化可能导致堆栈信息不完整 2. **静态函数**:需启用`-rdynamic`才能显示静态函数名 3. **动态链接库**:需确保符号表已加载(Windows需调用`SymInitialize`) --- ### 六、调试工具替代方案 - **GDB**:直接通过`bt`命令查看堆栈 - **Valgrind**:结合`--tool=memcheck`分析内存问题时的堆栈 根据需求选择合适方法,Linux优先推荐`backtrace`,Windows推荐`dbghelp`库。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值