栈参数不连续:手动实现vsprintf无法正确获取可变参数问题
问题原因分析
你最初的 sprintf 能正常工作,是因为32位x86环境下默认使用CDECL调用约定:函数参数从右往左压栈,可变参数紧跟在最后一个固定参数的栈位置之后,你按4字节偏移读取刚好匹配栈上的参数排列。重构后出问题的核心原因有两个:
- 你取到的
&back不是入参栈上的原始地址
函数内部直接对参数取地址时,编译器可能会把入参拷贝到栈帧的负偏移区域(也就是局部变量存储区),你观察到的color地址为-16(%ebp)就是这个现象,此时你拿到的&back是局部拷贝的地址,自然读取不到后面跟着的可变入参。 - 手动算偏移没有考虑栈对齐规则
哪怕是uint8_t、int16_t这类小于4字节的参数,32位gcc默认会按4字节栈对齐规则做填充,你直接按参数类型大小算偏移很容易错位。
正确实现方案
方案1:手动适配32位CDECL约定(仅学习用途)
你可以直接通过ebp寄存器计算可变参数的起始偏移,32位CDECL下栈帧结构固定:
%ebp存储旧栈帧基址%ebp+4存储返回地址- 从
%ebp+8开始依次存储函数的入参,每个参数都对齐到4字节
对应你的vga_printf函数,入参顺序从左到右为left、top、format、color、back,每个占4字节对齐空间,因此可变参数的起始地址为%ebp + 0x1C,你可以通过内嵌汇编拿到这个地址传给vsprintf:
void vga_printf(int16_t left, int16_t top, const char *format, uint8_t color, uint8_t back, ...) { static char buffer[256]; void* args; __asm__ __volatile__("movl %%ebp, %0" : "=r"(args)); args += 0x1C; vsprintf(buffer, format, args); vga_putstr(left, top, buffer, color, back); }
注意该方案仅适配32位CDECL环境,更换架构、开启优化、修改调用约定都会失效。
方案2:使用编译器内置可变参数宏(推荐)
直接用gcc提供的__builtin_va_list相关宏,编译器会自动处理不同架构、对齐规则、调用约定下的参数读取,不需要你手动计算偏移,实现起来兼容性和稳定性都更高:
// 若没有标准头文件可直接使用gcc内置的__builtin_va_list类型 typedef __builtin_va_list va_list; #define va_start(v,l) __builtin_va_start(v,l) #define va_arg(v,t) __builtin_va_arg(v,t) #define va_end(v) __builtin_va_end(v) void vsprintf(char *buf, const char *format, va_list args) { // 原有逻辑中读取参数的部分替换为va_arg(args, 目标类型)即可 // 例如读取int类型就写 int val = va_arg(args, int); } void vga_printf(int16_t left, int16_t top, const char *format, uint8_t color, uint8_t back, ...) { static char buffer[256]; va_list args; va_start(args, back); // 传入最后一个固定参数作为va_start的定位基准 vsprintf(buffer, format, args); va_end(args); vga_putstr(left, top, buffer, color, back); }
你自行测试切换到该方案后功能正常,也验证了这个方案的可靠性。
内容的提问来源于stack exchange,提问作者nekosu
相关产品推荐
相关产品推荐

