从反汇编理解堆栈及printf

本文深入探讨了C语言中printf函数的参数传递顺序及其内部实现原理,解析了不同数据类型在栈中的存储方式,并通过具体实例解释了参数从右至左入栈的过程。
#include <stdio.h>
int main()
{
    long long a = 1, b = 2, c = 3;
    printf("%d %d %d\n", a,b,c);
    return 0;
}

//Tencent某年实习生笔试题目


结果是:

1 0 2

Process returned 0 (0x0)   execution time : 0.136 s
Press any key to continue.


该段转自:

http://blog.youkuaiyun.com/yang_yulei/article/details/8086934


sprintf/fprintf/printf/sscanf/fscanf/scanf等这一类的函数,它们的调用规则(calling conventions)是cdecl,cdecl调用规则的函数,所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。函数参数的传递都是放在栈里面的,而且是从右边的参数开始压栈,printf()是不会对传递的参数进行类型检查的,它只有一个format specification fields的字符串,而参数是不定长的,所以也没办法对传递的参数做类型检查,也没办法对参数的个数进行检查。所以了,压栈的时候,参数列表里的所有参数都压入栈中了,它不知道有多少个参数,所以它都压栈。
所以对于问题1中的代码,压入栈之后应该是这样的:
栈里面压进去了三个数,a,b和c因为a占2*4个bytes,b占1*4个byte,所以现在栈里面有3*4个bytes。c、b先后压入栈,a最后压入栈。因为这是little endian,即每个数字的高字节在高地址,低字节在低地址。而栈的内存生长方向是从大到小的,也就是栈底是高地址,栈顶是低地址,所以a的低字节在低地址(低地址值为0x00000001,高地址值为0x00000000)。(有条件的同学可以在big endian的机器上验证一下)
那么输出的时候,format specification fields字符串其匹配栈里面的内容,首先一个%d取出4个bytes出来输出,然后后面又有一个%d再取出4个bytes出来打印。所以结果就是这样了。也就是说刚开始压入栈的b的值在输出的时候根本都没有用到,c更没用到。


printf在压栈时,对于长度小于32位的参数,自动扩展成32位(由CPU的位数决定的)。
故在根据格式串解释时,对于%c %hd这样的小于32位数据的格式串,系统也会自动提取32位数据解释,而不会提取8位或16位来解释。(因为你把人家压入的时候就规定了扩展成32位嘛),但输出结果仍是8位或者16位值
至于浮点参数压栈的规则:float(4 字节)类型扩展成double(8 字节)入栈。所以在输入时,需要区分float(%f)与double(%lf),而在输出时,用%f即可。printf函数将按照double型的规则对压入堆栈的float(已扩展成double)和double型数据进行输出。

另附一段程序的反汇编作为理解;

0x00401334 push   %ebp    //ebp入栈作为保护
0x00401335 mov    %esp,%ebp //栈顶地址给ebp,ebp是读取栈内容的寄存器
0x00401337 and    $0xfffffff0,%esp
0x0040133A sub    $0x40,%esp
0x0040133D call   0x401970 <__main>
0x00401342 movl   $0x1,0x38(%esp) //栈底是0x1,+4空间是0x0
0x0040134A movl   $0x0,0x3c(%esp)
0x00401352 movl   $0x2,0x30(%esp)
0x0040135A movl   $0x0,0x34(%esp)
0x00401362 movl   $0x3,0x28(%esp)
0x0040136A movl   $0x0,0x2c(%esp) //上面到main是按定义顺序入栈,这里是小头先入栈,后面会做调整
0x00401372 mov    0x28(%esp),%eax //以下为调整
0x00401376 mov    0x2c(%esp),%edx
0x0040137A mov    %eax,0x14(%esp)
0x0040137E mov    %edx,0x18(%esp) //按照大地址先入栈的顺序入栈,先入c
0x00401382 mov    0x30(%esp),%eax
0x00401386 mov    0x34(%esp),%edx
0x0040138A mov    %eax,0xc(%esp)
0x0040138E mov    %edx,0x10(%esp) //b
0x00401392 mov    0x38(%esp),%eax
0x00401396 mov    0x3c(%esp),%edx
0x0040139A mov    %eax,0x4(%esp)
0x0040139E mov    %edx,0x8(%esp) //最后入栈a,调整后的顺序是c b a,大地址在栈底,a最先定义可以先出栈
0x004013A2 movl   $0x403024,(%esp)
0x004013A9 call   0x401be0 <printf> //下面是printf,就按之前的说明执行
0x004013AE mov    $0x0,%eax
0x004013B3 leave
0x004013B4 ret


在STM32上实现进入 `HardFault_Handler` 时打印堆栈信息,是一种有效的调试方式,可以帮助定位程序崩溃的原因。通过分析堆栈内容,可以获取发生异常时的函数调用链、寄存器状态以及程序计数器等关键信息。以下是实现该功能的详细步骤和原理。 ### 堆栈结构与异常处理机制 在 Cortex-M3/M4 内核中,当发生异常(如 HardFault)时,内核会自动将一些寄存器压入堆栈,包括 R0~R3、R12、LR(链接寄存器)、PC(程序计数器)和 xPSR(程序状态寄存器)。这些信息对于调试非常关键,因为它们记录了异常发生时的上下文环境 [^2]。 ### 实现方法 在 `HardFault_Handler` 中,可以通过汇编代码将堆栈指针(MSP 或 PSP)传递给 C 函数进行解析。以下是一个典型的实现示例: ```armasm HardFault_Handler: TST LR, #4 ITE EQ MRSEQ R0, MSP MRSNE R0, PSP B HardFault_Handler_C ``` 上述汇编代码判断当前使用的堆栈指针是主堆栈指针(MSP)还是进程堆栈指针(PSP),并将该指针传递给 C 函数 `HardFault_Handler_C` 进行进一步处理 [^2]。 ### C 函数解析堆栈信息 在 C 函数中,可以访问堆栈中的寄存器值,并打印关键信息,例如 PC(程序计数器)、LR(链接寄存器)、xPSR 和其他通用寄存器: ```c void HardFault_Handler_C(unsigned int *stack_frame) { unsigned int r0 = stack_frame[0]; unsigned int r1 = stack_frame[1]; unsigned int r2 = stack_frame[2]; unsigned int r3 = stack_frame[3]; unsigned int r12 = stack_frame[4]; unsigned int lr = stack_frame[5]; // Link Register unsigned int pc = stack_frame[6]; // Program Counter unsigned int psr = stack_frame[7]; // Program Status Register printf("HardFault occurred:\r\n"); printf("R0 = 0x%08X\r\n", r0); printf("R1 = 0x%08X\r\n", r1); printf("R2 = 0x%08X\r\n", r2); printf("R3 = 0x%08X\r\n", r3); printf("R12 = 0x%08X\r\n", r12); printf("LR = 0x%08X\r\n", lr); printf("PC = 0x%08X\r\n", pc); printf("PSR = 0x%08X\r\n", psr); while (1); // 停留在此处,便于调试器捕获 } ``` 通过打印这些寄存器的值,可以定位到异常发生时的程序位置(PC 值),并结合反汇编工具(如 `objdump` 或调试器中的 Disassembly 窗口)找到具体的源代码位置 [^3]。 ### 配合调试工具 在实际调试中,除了打印堆栈信息外,还可以使用调试器(如 STM32CubeIDE 或 Keil MDK)查看调用反汇编代码以及内存内容。例如,在 STM32CubeIDE 中,可以将 PC 值输入到 Disassembly 窗口的地址栏中,定位到具体的汇编指令,从而找到源代码行号 [^3]。 ### 注意事项 - **堆栈溢出**:如果堆栈空间不足,可能导致堆栈信息被破坏,从而无法正确解析异常上下文。因此,应确保任务或线程的堆栈大小足够,特别是在使用 FreeRTOS 等实时操作系统时 [^1]。 - **调试输出方式**:`printf` 函数在 HardFault 处理中可能不可靠,建议使用低级的串口发送函数(如直接操作 USART 寄存器)来确保信息能够输出。 - **优化级别**:编译器优化可能会影响堆栈信息的准确性,调试时建议关闭优化(-O0)以获得更清晰的调用。 ### 示例代码 以下是完整的实现代码: #### 汇编部分(启动文件中) ```armasm HardFault_Handler: TST LR, #4 ITE EQ MRSEQ R0, MSP MRSNE R0, PSP B HardFault_Handler_C ``` #### C 函数部分 ```c void HardFault_Handler_C(unsigned int *stack_frame) { unsigned int r0 = stack_frame[0]; unsigned int r1 = stack_frame[1]; unsigned int r2 = stack_frame[2]; unsigned int r3 = stack_frame[3]; unsigned int r12 = stack_frame[4]; unsigned int lr = stack_frame[5]; unsigned int pc = stack_frame[6]; unsigned int psr = stack_frame[7]; printf("HardFault occurred:\r\n"); printf("R0 = 0x%08X\r\n", r0); printf("R1 = 0x%08X\r\n", r1); printf("R2 = 0x%08X\r\n", r2); printf("R3 = 0x%08X\r\n", r3); printf("R12 = 0x%08X\r\n", r12); printf("LR = 0x%08X\r\n", lr); printf("PC = 0x%08X\r\n", pc); printf("PSR = 0x%08X\r\n", psr); while (1); // 停留在此处,便于调试器捕获 } ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值