c/c++中入栈顺序

c/c++中的都是从右往左入栈的,在调用函数时,若参数需要运算则先运算,然后入栈,再调用函数。


1. 入栈顺序

程序中栈顶在低位,栈低在高位。
程序入栈顺序:

void push(int a, int b, int c)
{
    printf("a = %d, ptr = 0X%x\r\n", a, &a);
    printf("b = %d, ptr = 0X%x\r\n", b, &b);
    printf("c = %d, ptr = 0X%x\r\n", c, &c);
}

在这里插入图片描述
先入栈的在栈底,应该存储在高位;后入栈的在栈顶,应该存储在低位。
位置顺序:c > b > a
所以:c最先入栈,a最后入栈
函数调用是从右往左入栈的


2. 函数执行顺序

  1. 函数参数有运算,则先计算完参数中的全部运算再执行函数
  2. 函数参数有运算需要区分是调用前执行还是调用后执行的函数(++i,i++)
  3. 入栈的是临时变量还是实际变量

分析下面代码的运算结果:

int main()
{
    /*
    1. 入栈顺序,参数从右往左入栈
    2. 计算顺序,先入栈的先计算;计算完后再入栈
    3. 传递的参数:实际变量,还是临时变量
    */
	int i=1;
    // 先计算,两次i++后i的值为3;传递的是实际变量i的值
    printf("%d,%d\n", ++i, ++i); //3,3
    // i++入栈的是i,然后执行i++,所以需要临时变量将i的值保存下来,再计算i++;++i先进行计算,入栈为变量i的值
    printf("%d,%d\n", ++i, i++); //5,3
    /*
    1.后一个i++保存临时变量i2=5,然后i自增=6
    2.前一个i++先保存临时变量i1=6,然后i自增=7
    3.右边的参数先入栈push i2
    4.左边的参数入栈push i1
    */
    printf("%d,%d\n", i++, i++); //6,5
    /*
    1.后一个++i,先自增i=8
    2.前一个i++;先保存临时变量i1=8,然后i自增=9
    3.右边入栈push i
    4.左边入栈push i1
    */
    printf("%d,%d\n", i++, ++i); //8,9
}

也可从编译后的汇编代码中看出:

# printf("%d,%d\n", ++i, ++i);

# 取出i的值到寄存器
	mov	eax, DWORD PTR _i$[ebp]
# 寄存器自增1
	add	eax, 1
# 写回到i
	mov	DWORD PTR _i$[ebp], eax
# 取出i的值到寄存器
	mov	ecx, DWORD PTR _i$[ebp]
# 寄存器自增1
	add	ecx, 1
# 写回到i
	mov	DWORD PTR _i$[ebp], ecx
# 读取i的值到寄存
	mov	edx, DWORD PTR _i$[ebp]
# 入栈
	push	edx
# 读取i的值到寄存
	mov	eax, DWORD PTR _i$[ebp]
# 入栈
	push	eax
	push	OFFSET $SG3750
# 调用_printf
	call	_printf

# printf("%d,%d\n", ++i, i++);
# 取出i的值
	mov	ecx, DWORD PTR _i$[ebp]
# 保存到临时变量
	mov	DWORD PTR tv71[ebp], ecx
# 取出i的值,并自增1,写回i
	mov	edx, DWORD PTR _i$[ebp]
	add	edx, 1
	mov	DWORD PTR _i$[ebp], edx
# 取出临时变量的值,并入栈
	mov	eax, DWORD PTR tv71[ebp]
	push	eax
	mov	ecx, DWORD PTR _i$[ebp]
	push	ecx
	push	OFFSET $SG3751
	call	_printf

再思考一下下面的代码执行结果:

#include <stdio.h>

void push(int a, int b, int c) { printf("%d,%d,%d\n", a, b, c); }

int main() {
  int i = 0;
  push(i++, i++, i++);	// 输出:0,0,0
  i = 0;
  push(++i, ++i, ++i);  // 输出:3,3,3
  i = 0;
  push(++i, i++, ++i);  // 输出:2,1,2
  i = 0;
  push(i++, ++i, ++i);  // 输出:2,2,2
}

// 以上结果是使用msvc++ 12.0 / vc++ 2013编译的,gcc(gcc version 9.3.0)编译的结果如下:
/*
2,1,0		// 此结果可以看出gcc中参数从右往左入参
3,3,3
3,1,3
2,3,3
*/


关于上面的代码msvc编译和gcc编译得到的结果不同,我又把两个的汇编代码搞出来看了一下:

# 只看 main 开始到 push(i++, i++, i++); 
# mscv ( Microsoft Visual Studio 12.0 / msvc++ 2013 )
_main	PROC
; File d:\users\administrator\desktop\main.cpp
; Line 5
	push	ebp
	mov	ebp, esp
	sub	esp, 24					; 00000018H
; Line 6
	mov	DWORD PTR _i$[ebp], 0	# 赋值 0 
; Line 7
	mov	eax, DWORD PTR _i$[ebp]
	mov	DWORD PTR tv65[ebp], eax
	mov	ecx, DWORD PTR _i$[ebp]
	mov	DWORD PTR tv68[ebp], ecx
	mov	edx, DWORD PTR _i$[ebp]
	mov	DWORD PTR tv71[ebp], edx
	mov	eax, DWORD PTR tv65[ebp]
	push	eax
	mov	ecx, DWORD PTR tv68[ebp]
	push	ecx
	mov	edx, DWORD PTR tv71[ebp]
	push	edx
	call	?push@@YGXHHH@Z				; push	# push之前传参,push之后自增+1
	mov	eax, DWORD PTR _i$[ebp]
	add	eax, 1
	mov	DWORD PTR _i$[ebp], eax
	mov	ecx, DWORD PTR _i$[ebp]
	add	ecx, 1
	mov	DWORD PTR _i$[ebp], ecx
	mov	edx, DWORD PTR _i$[ebp]
	add	edx, 1
	mov	DWORD PTR _i$[ebp], edx


# gcc ( gcc version 9.3.0 )
main:
.LFB1:
	.loc 1 5 12
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	.loc 1 6 7
	movl	$0, -4(%rbp)	# i = 0
	.loc 1 7 7
	movl	-4(%rbp), %edx	# edx赋值,保存栈地址
	leal	1(%rdx), %eax	# eax = rdx+1
	movl	%eax, -4(%rbp)	# exa入栈
	movl	-4(%rbp), %ecx
	leal	1(%rcx), %eax
	movl	%eax, -4(%rbp)	# 
	movl	-4(%rbp), %eax
	leal	1(%rax), %esi
	movl	%esi, -4(%rbp)
	movl	%ecx, %esi
	movl	%eax, %edi
	call	_Z4pushiii	# 先运算,后调用push

函数参数列表中表达式的运算顺序并没有在C/C++中明确定义,每个编译器可以自由发挥,有自己的实现。至少我测试的gcc 9.3.0和vc++ 2013测试的结果是不一致的。


3. 分析为什么入栈顺序是从右往左入栈

很多答案都提到是为了函数可以使用动态参数。

动态参数是如何实现的?
函数是通过栈传入参数的,动态参数需要压入个数和类型都未知的参数到栈中。而我们知道第一个参数一定是最后一个入栈的,在其他参数之上。

  1. 如何读取动态参数?
    参数肯定在栈里,但是要如何读取到栈中的参数呢?
    熟悉汇编的可能会考虑通过ESP寄存器,因为ESP指向栈顶位置。但是在函数内,第一个参数一定不是在栈顶位置:
  • 在参数是在函数调用前入栈的
  • 进入函数后,需1. 之前的栈底EBP入栈;2. 修改栈底值为当前栈顶值,用于函数调用完后的栈平衡

所以第一个参数的地址最接近ESP也只能是ESP-12(ESP指向栈顶,即下一个入栈的位置,栈是自高地址向低地址增长,所以是-12,需要多大内存就减多少)

如果没有在函数最开始就获取第一个参数的地址,在程序运行中ESP还可能会变,所以通过ESP获取一个参数的位置是不可取的,而且不同的编译器实现还不一定都一样。

// 通过ESP读取参数值
void push(int a, int b, int c)
{
    int i1;
    __asm
        {
        mov eax,esp
        mov	eax,[esp+12]
        mov	i1,eax
        }
    printf("a = %d\r\n", i1);
    printf("a = %d, ptr = 0X%x\r\n", a, &a);
    printf("b = %d, ptr = 0X%x\r\n", b, &b);
    printf("c = %d, ptr = 0X%x\r\n", c, &c);
}

通过上面的分析,要实现动态参数,我们需要有一个确切地址的参数,用来定位其他参数。

所以在c++的实现中,带有动态参数的函数,第一个参数一定是个很明确的参数。
而且参数从右往左入栈,第一个参数位置就非常明确,且可以快速定位第一个参数的位置(ESP+12)。

结论:

  1. 参数从右往左入栈可以快速确定第一个参数的位置,非常适合带动态参数的函数
  2. 从左往右入参可以快速定位最后一个参数的位置;但如果是动态参数,则无法定位最后一个参数的位置(类型不确定,无法明确参数size)

动态参数的缺陷:

  1. 无法确定参数长度
  2. 无法确定参数类型

printf是通过格式化来确定输出的参数长度和类型。

4. stdcall、cdecl、fastcall传参入栈方式

在写dll时经常会用stdcall、cdecl声明函数,为什么要这样?其实就是声明函数参数的传递及销毁方式。
他们之间的区别:

  • stdcall:参数从右往左入栈,在函数返回前由被调用者清理栈(恢复到调用前的栈状态);C++默认调用方式。
  • cdecl:参数从右往左入栈,由调用者清理栈;是C语言的默认函数调用方式。
  • fastcall:一种快速调用方式,前两个参数由寄存器ecx、edx来传递,其余参数由栈传递;不同编译器的实现有不同。

5. 类成员函数this指针传递方式

类的成员函数和非成员函数及普通函数在编译后都是一样的,类成员函数特殊的地方在于函数调用时会通过寄存器(ecx)传递this指针。

参考

Compiler Explorer - 在线查看汇编工具

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值