注意:都是在没有优化的情况下编译的。因为只要开-O1或是-O2,那么汇编代码就少的可怜了,都被优化掉了
编译器版本:x86-64 gcc 5.5
1 POD类型传参
1.1 一个pod参数,pod返回值
int square(int num) {
return num * num;
}
int main()
{
int c=90;
int a=square(c);
++a;
}
对应汇编

1.2 两个pod参数,pod返回值
int mul(int v1,int v2) {
return v1 * v2;
}
int main()
{
int c=90;
int a=mul(c,1);
++a;
}

当第二个参数也传入变量的时候,会使用edx,像eax一样传入。然后返回值依然使用eax返回。
1.3 几个参数才会动用到栈传参,
int mul(int v1,int v2,int v3,int v4,int v5,int v6,int v7) {
return v1 * v2*v3*v4*v5*v6*v7;
}
int main()
{
int c1=90;
int c2=10;
int c3=10;
int c4=10;
int c5=10;
int c6=10;
int c7=10;
int a=mul(c1,c2,c3,c4,c5,c6,c7);
++a;
c1++;c2++;c3++;c4++;c5++;c6++;c7++;
a=mul(a,c2,c3,c4,c5,c6,c7);
}

从上图可以看到,是从第7个参数开始,使用栈传递参数。
并且之前都是使用edi作为第一个参数,但是当使用栈的时候就使用edi来倒腾数据了。
注意,栈先回退,然后再去保存返回值
再看函数调用:

2 结构体传参
class A
{
public:
A()
:i1(1)
{}
public:
int i1;
char a;
int i2;
char ca;
char c1;
~A()
{
i2=2;
}
};
int func(A a1,A a2,A a3,A a4,A a5,A a6,A a7)
{
a1.a++;
return 1;
}
void func1(int i)
{
if(i<10)
{
i++;
A a1;
A a2;
A a3;
A a4;
A a5;
A a6;
A a7;
int a=func(a1,a2,a3,a4,a5,a6,a7);
// a1.a++;
// int bb=func(a1,a2,a3,a4,a5,a6);
// }else{
// return func1(i+1);
// }
}
}
先看func1函数

传参

加上一个拷贝构造函数:
A(A& a)
{
ca=++a.ca;
}

然后看看拷贝构造函数的汇编:

对的。你有没有发现,对象a,也就是参数所在位置的内存,就只有ca成员被初始化了,也就是说被修改了。其他的数据成员都没有修改。
然后配合

这种,栈指针回退的做法,是不是就能够明白。为什么说函数内部的变量,如果没有初始化,那么值是未定义的。而不是说0
因为,之前使用这块内存空间的函数,并没有将那块内存空间清0,而是直接sp+8这种形式退了回去。
因此后面函数再使用相同的内存,那么就是未定义啊!!!未定义啊!
明白是怎么来的了吗?
为啥说函数外的变量就不会这样。因为函数外变量占用的内存就不会被回收,也就不存在被重用。一定是0.它内部的内存一定是0啊~~(那如果是别的进程使用了内存那?操作系统可能会清0吧。这个就真不清楚了)
然后继续看函数调用

上面 为什么要 先 rsp -8 ,命名push 的时候可以自动的做到rsp-8。
这里为啥,我没想明白。但是当压入8个参数的时候,也就是说2个参数需要栈传参,那么会sp-8*2
再来看一下func函数。改一下,不然有些信息看不出来。
int func(A a1,A a2,A a3,A a4,A a5,A a6,A a7,A b)
{
b.a++;
a1.a++;
return 1;
}

虽然编译器是gcc,但是因为有类,因此最终还是用的 g++ 来编译。
因此,从上面可以看出来了吧,其实参数构造的地方,是在栈上,但是函数实际使用的是 lea 指令取的参数的有效地址,然后保存在寄存器中,被调用函数通过寄存器访问参数。
也就是说,并不存在什么值传递。值传递的本意是参数使用拷贝构造函数复制了一份。然后取其有效地址通过寄存器传入了被调用函数
那拷贝一份的意义在哪?在于不会更改原来的变量的值。应为传入的参数是构造函数复制的那份。更改也无所谓。
如果第8个参数改为指针传参会发生什么:
首先发生的变化就是,拷贝构造函数的调用少了一次,也就是说第8个参数没有被拷贝,但是第8个参数需要通过压栈传参了,因此可以发现,直接压栈第八个参数实际值的有效地址

2.2 结构体类型返回值
首先
class A
{
public:
A()
: i(1)
{
}
A(A& a)
{
}
int i;
// int i1;
// int i2;
};
A func()
{
A a;
return a;
}
void func1()
{
A b = func();
}
这段代码编译不通过:
为什么,因为sp指针在call结束以后直接回退,返回值的处理是在sp指针回退以后才开始进行的。
那也就是说,如果这段代码可以通过编译,那么就是说明,eax寄存器存储的是返回值的有效地址,而这个有效地址已经在sp指针之下了,也就是说不在栈内了。
换句话说,这个元素已经不可用了。既然已经不可用了,那怎么还能用一个不可用的变量来构造值??
其实问题是在于,这个值是非const传递的,a在栈回退以后是一个匿名变量了,也就是说是一个右值了。她已经不再栈上了(sp之下),因此也就是无法被修改了。
而拷贝构造函数,不可避免的会携带之前变量的一些值,也就是说,是可以修改原来的变量的。但是a已经是一个右值了,(其实是在sp之下了),无法被修改。因此编译器拒绝了这种构造。
但是如果改为
class A
{
public:
A()
: i(1)
{
}
A(const A &a)
{
}
int i;
// int i1;
// int i2;
};
A func()
{
A a;
return a;
}
void func1()
{
A b = func();
}
编译就可以通过,为什么。因为传入的是const A& 意味着使用这个值构造的对象,不会去修改这个值。或是说不能被修改。因此就可以使用这个值去构造其他对象了。
但是问题还是一个,a已经不再栈上了,怎么去构造?
答案是先预留出返回值的内存空间,然后将这个地址传入,在被调用函数中构造。

3 nop是什么
这个,没看完,不太懂。贴个链接吧。