从反汇编理解堆栈及printf

本文深入探讨了C语言中printf函数的参数传递顺序及其内部实现原理,解析了不同数据类型在栈中的存储方式,并通过具体实例解释了参数从右至左入栈的过程。

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

#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


<think>嗯,用户现在问的是C语言中usleep(50*1000)调用导致堆栈异常的问题。首先,我需要确认usleep的基本功能。usleep函数用于让程序休眠指定的微秒数,50*1000也就是50毫秒。通常来说,usleep本身不会直接导致堆栈异常,但可能某些情况下间接引发问题。 接下来,用户提到的堆栈异常可能是什么原因呢?可能的方向有几个。首先是堆栈溢出,比如在递归函数中频繁调用usleep,导致堆栈空间不足。其次是多线程问题,如果多个线程同时操作共享数据而没有正确同步,使用usleep可能让竞争条件更容易暴露。还有可能是信号处理函数中调用了不可重入函数,而usleep可能被信号中断,导致意外行为。 然后,我需要考虑usleep的可移植性问题,因为它已经被弃用,推荐使用nanosleep。用户可能在旧代码中使用了usleep,而新环境中存在兼容性问题,引发未定义行为,间接导致堆栈错误。 另外,堆栈损坏的可能性也不能排除。比如,数组越界、指针错误等内存问题破坏了堆栈,而usleep的位置恰好让问题显现出来。这时候需要检查代码中的内存操作,是否有缓冲区溢出或使用野指针的情况。 还有,信号处理的问题。如果程序在处理信号时,尤其是在信号处理函数中执行了非异步安全的操作,可能会破坏堆栈结构。usleep在休眠时可能被信号打断,如果信号处理函数有问题,可能引发异常。 针对这些可能性,我应该建议用户如何排查呢?首先检查堆栈使用情况,查看是否有无限递归或过大的局部变量。使用调试工具如GDB来分析崩溃时的堆栈信息。检查多线程同步机制,确保共享数据的正确访问。替换usleep为nanosleep,测试是否问题依旧。使用内存检测工具如Valgrind来发现内存错误。最后,检查信号处理函数是否符合规范,避免非异步安全操作。 用户可能需要具体的示例代码来演示问题和排查步骤,比如展示一个递归导致的堆栈溢出,或者多线程竞争的例子。此外,提醒用户注意usleep的返回值,确保正确处理中断情况,避免忽略错误导致的问题累积。 总结来说,usleep本身不直接导致堆栈异常,但可能暴露代码中的潜在问题。需要从堆栈溢出、多线程同步、内存错误、信号处理等方面逐一排查,结合工具进行分析和验证。</think>在 C 语言中使用 `usleep(50*1000)`(休眠 50 毫秒)时遇到调用堆栈异常,可能与以下原因相关。以下是详细分析和排查方向: --- ### 一、`usleep` 基础特性 1. **函数定义** ```c #include <unistd.h> int usleep(useconds_t usec); // 微秒级休眠(1秒=1,000,000微秒) ``` - `usleep(50*1000)` 表示休眠 **50 毫秒** - **已弃用**:POSIX 标准中 `usleep` 已被标记为过时,建议改用 `nanosleep` 2. **常见行为** - 若被信号中断会 **提前返回**(返回 `-1`,错误码 `EINTR`) - 不会主动修改堆栈,但可能间接暴露代码中的隐藏问题 --- ### 二、堆栈异常的可能原因 #### 1. **堆栈溢出(Stack Overflow)** - **典型场景** ```c void recursive_func() { char buffer[1024]; // 大局部变量占用堆栈 usleep(50*1000); // 休眠期间堆栈未释放 recursive_func(); // 无限递归导致堆栈耗尽 } ``` - **表现**:段错误(Segmentation Fault)或 `SIGSEGV` 信号 - **排查方法** - 检查是否在递归或深层嵌套调用中使用 `usleep` - 通过 `ulimit -s` 查看系统默认堆栈大小(通常 8MB) - 使用 `-fstack-usage` 编译选项分析堆栈使用 #### 2. **多线程同步问题** - **竞态条件(Race Condition)** ```c void* thread_func(void* arg) { static int shared_data = 0; usleep(50*1000); // 休眠期间其他线程可能修改 shared_data shared_data++; // 未加锁的共享数据访问 return NULL; } ``` - **表现**:数据损坏或堆栈指针异常 - **排查方法** - 使用互斥锁(`pthread_mutex`)保护共享资源 - 通过 `valgrind --tool=helgrind` 检测线程竞争 #### 3. **信号处理干扰** - **异步信号安全问题** ```c void signal_handler(int sig) { printf("Signal received\n"); // 不可重入函数(非异步安全) } int main() { signal(SIGINT, signal_handler); usleep(50*1000); // 休眠期间可能被信号中断 return 0; } ``` - **表现**:堆栈被信号处理函数破坏 - **排查方法** - 确保信号处理函数仅调用异步安全函数(如 `write` 替代 `printf`) - 使用 `sigaction` 替代 `signal` 注册信号处理 #### 4. **内存越界污染堆栈** - **缓冲区溢出** ```c void vulnerable_func() { char buffer[16]; memset(buffer, 0, 32); // 越界写入破坏堆栈 usleep(50*1000); // 异常在休眠后触发 } ``` - **表现**:休眠后出现随机崩溃 - **排查方法** - 使用 `-fsanitize=address` 编译选项检测内存越界 - 通过 `gdb` 查看崩溃时的堆栈反汇编 --- ### 三、调试与修复步骤 #### 1. **基本调试工具** - **GDB 分析崩溃现场** ```bash gdb ./your_program (gdb) run (gdb) bt full # 查看完整堆栈 (gdb) info locals # 检查局部变量 ``` - **Valgrind 检测内存错误** ```bash valgrind --leak-check=full ./your_program ``` #### 2. **替代 `usleep` 测试** ```c #include <time.h> void safe_delay(int ms) { struct timespec ts = { ms / 1000, (ms % 1000) * 1000000 }; nanosleep(&ts, NULL); // 更安全的替代方案 } ``` #### 3. **堆栈保护编译选项** ```bash gcc -fstack-protector-strong -o program program.c ``` --- ### 四、关键注意事项 1. **避免在信号处理函数中使用 `usleep`** 2. **多线程代码需确保资源同步** 3. **优先使用 `nanosleep` 替代已弃用的 `usleep`** 通过上述方法定位到具体原因后,可针对性地修复代码逻辑或内存操作问题。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值