printf函数的压栈顺序是从右到左压栈,计算表达式值的顺序也是从右到左,由于输入数据类型的多样性,压栈的时候将数据或数据的地址mov到寄存器中,然后将寄存器压栈

例子:

#include <iostream>
using namespace std;
int main(){
	int arr[] = { 6, 7, 8, 9, 10 };
	int *ptr = arr; //ptr指向6
	*(ptr++) += 123;//先执行6+123,然后ptr指向7
	printf("%d,%d\n", *ptr, *(++ptr));//先执行ptr+4使得ptr指向8,然后把ptr压栈2次
	printf("%d,%d,%d,%d,%d\n", *ptr--,*ptr+20, *(ptr--), *ptr, *(++ptr));
	int i = 5;
	char ch = 't';
	char str[10] = "test";
	printf("%s %c %d\n", str, ch, i);
	printf("%d %d %d %d\n", i, --i, i, i--);
	system("pause");
	return 0;
}

程序在VS2013中的输出如下:

javascript压栈出栈 printf压栈出栈_寄存器

分析:

javascript压栈出栈 printf压栈出栈_寄存器_02


上述图片中是调用printf函数时的汇编代码,主要流程是:将ptr指针增加4B,保存main的栈顶指针,把printf函数所需的2个参数压栈,调用printf函数,恢复main函数的帧顶指针。

dword表示指针是4B的,dword ptr [X]表示X所指地址对应的数据类型是双字指针。

move esi,esp表示保存main函数的栈帧的栈顶地址,以便调用printf函数返回时回到该位置处。

call dword ptr ds:[0C82184h]表示data segment寄存器中地址向后偏移0C82184h处的双字指针,此处是printf函数的代码段,call表示调用printf函数。

00C73729至00C7372E是VS编译器自带的检查栈帧是否正确恢复到原始地址的,而esp是栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶,在printf函数随着push操作而改变,因为前面往栈中push了3次,由于栈向地址减少的方向增长,那么esp就减少了12,printf结束时esp要回到mian函数的栈顶,那就是add esp, 0Ch 了。cmp指令比较esp值与esi值,结果不保存,只是修改标志寄存器值。最后__RTC_CheckEsp是调试的时候VS编译器自己加入的,发布版本会过滤掉此函数 功能就是检查堆栈平。

javascript压栈出栈 printf压栈出栈_printf压栈_03


从上述图片中的汇编代码中可以看出,对printf函数的参数压栈过程是:

1)先从右到左计算ptr指针的变化,并把ptr–操作的结果地址保存到临时寄存器中;该步骤到00E83764 mov esi,esp结束;

2)为printf函数执行参数压栈操作,对于ptr–操作从临时寄存器中取出地址并压栈,对于其他*ptr操作和++ptr则使用ptr的最新值并压栈。

似乎也可以从中看出printf函数参数中,(++ptr)和ptr–操作的区别是:前缀++在压栈前执行,压栈时使用最新ptr值;而后缀–操作在压栈中进行并保持操作结果,压栈时使用保存的ptr值。
因此最后输出值为:8,27,9,7,7.

javascript压栈出栈 printf压栈出栈_后缀_04

javascript压栈出栈 printf压栈出栈_javascript压栈出栈_05


根据前面的解释:前缀操作在压栈前执行,压栈时使用最新指针值;后缀操作也在压栈前进行但保存结果到临时寄存器,压栈时压入临时寄存器的指针。

那么上面myprintf("%d,%d,%d,%d\n",i,–i,i,i–)代码中,从右往左执行,最后一个参数是后缀i–操作,把i的地址保存到临时寄存器,值为5;然后i的值减为4;倒数第二个参数为i,地址不用变;倒数第3个操作为–i,先把值减1得3,地址不保存;第一个操作为i,地址不保存。压栈时从右到左压栈,倒数第一个操作为i–,后缀操作,取临时寄存器的地址,值为5,压栈;倒数第2个操作,取i的最新地址,值为3,压栈;第2个参数和第1个参数都是取i的最新值,即3,压栈。输出时从栈顶到栈底依次输出,结果为:5,3,3,3。

说明:

真正开始压栈操作是从mov esi,esp开始。
eax是加法寄存器
mov eax,dword ptr[ptr] 是源操作数的间接寻址。

360笔试题如下

int a[] = { 3, 4 }, b[] = { 5, 6 };
struct st{
	int num;
	int *p;
}s[2] = {100,a,100,b},*q = s;
int main(){
	int n;
	printf("%d%d%d",*q->p,++q->num,(q++)->num);
	return 0;
}

输出:5101100

解释:压栈前,计算q的地址变化,q初始时指向s[0],在printf函数的参数中,首先是q后缀++操作,需要先保存q的地址到临时寄存器中,然后执q++,这时q指向S[1],并把地址保存;然后是++q->num操作,++操作优先级低于->操作,因此是把s[1].num进行加运算,值由100变为101,修改的不是q的值;最后是*q->p操作,q地址没变;压栈时,从右到左,首先取临时寄存器中q的地址,即S[0].num,值100压栈;最新q指向s[1],然后取s[1].num,值为101压栈;最后取s[1].p的值,s[1].p指向数组b,值为5压栈。最后从栈顶到栈底输出,即:5,101,100.