写在前面:突然心血来潮,想给读者写一篇关于汇编层面来理解值传递和引用传递的区别的帖子。
版本信息:Linux系统(centos7)、x86汇编、32位系统、gcc 4.8.5.
add_value方法:传入2个int变量(值)。
add_reference方法:传入2个int指针(地址)。
- #include
-
- int add_value(int a,int b){
- int c = a;
- int d = b;
- return c + d;
- };
- int add_reference(int *a,int *b){
- int *c = a;
- int *d = b;
- return (*c) + (*d);
- }
-
- int main(){
- int a = 1;
- int b = 2;
- add_value(a,b);
- add_reference(&a,&b);
- return 1;
- }
通过gcc命令编译c语言,生成对应的汇编代码。
gcc -S -m32 demo.c
main方法汇编:
- main:
- pushl %ebp # 把ebp寄存器存放的地址压栈,目的为了返回
- movl %esp, %ebp # 让esp和ebo保持在一个位置。
- subl $24, %esp # 开辟24个字节大小,也就是esp的地址往底地址推24个地址。
- movl $1, -4(%ebp) # 把立即数1放入到ebp-4的位置(放入栈中)
- movl $2, -8(%ebp) # 把立即数2放入到ebp-8的位置(放入栈中)
- movl -8(%ebp), %edx # 把ebp-8位置的值放入到edx寄存器中(目的是为了传递值)
- movl -4(%ebp), %eax # 把ebp-4位置的值放入到eax寄存器中(目的是为了传递值)
- movl %edx, 4(%esp) # 把edx寄存器的值放入到esp-4的位置(目的是通过栈传参给方法)
- movl %eax, (%esp) # 把eax寄存器的值放入到esp的位置(目的是通过栈传参给方法)
- call add_value # 调用add_value。
- leal -8(%ebp), %eax # 把ebp-8的地址放入到eax寄存器中(注意这里放入的是地址)
- movl %eax, 4(%esp) # 把eax寄存器存放的地址放入到esp+4的位置中(目的是通过栈传参给方法)
- leal -4(%ebp), %eax # 把ebp-4的地址放入到eax寄存器中(注意这里放入的是地址)
- movl %eax, (%esp) # 把eax寄存器存放的地址放入到esp的位置中(目的是通过栈传参给方法)
- call add_reference # 调用add_reference
- movl $1, %eax
- leave
- ret
lea指令:获取到地址,移动地址。
call指令:调用方法
add_value方法汇编:
- add_value:
- pushl %ebp # 将上个栈帧的ebp地址压入当前栈帧(为了返回)
- movl %esp, %ebp # 将esp和ebp保持在同一个位置
- subl $16, %esp # 在栈上开辟16个字节大小空间。
- movl 8(%ebp), %eax # 把ebp+8地址的值移动到eax寄存器中(栈传值)
- movl %eax, -4(%ebp) # 把eax寄存器的值放入到ebp-4的位置。
- movl 12(%ebp), %eax # 把ebp+12地址的值移动到eax寄存器中(栈传值)
- movl %eax, -8(%ebp) # 把eax寄存器的值放入到ebp-8的位置。
- movl -8(%ebp), %eax # 把ebp-8的位置的值移动到eax寄存器中
- movl -4(%ebp), %edx # 把ebp-4的位置的值移动到edx寄存器中
- addl %edx, %eax # 把edx寄存器的值和eax寄存器的值相加。并且放到eax寄存器中。
- popl %ebp # 将ebp的值弹出,这样就知道上一个栈帧的位置
- ret # 将eip的值弹出,这样就知道下一行执行的代码位置
注释特别特别的详细了。但是用注释+图片来理解更佳,所以看到下图。
在32位机中,方法的传参是通过栈的方法来传递。在32位机中,地址和int都是占用4个字节,所以在add_value开辟的栈帧中,直接用ebp寄存器+8和+12就可以获取到main方法中准备好的方法参数。而通过汇编可以得知,这里是mov的真实值,并没有通过lea指令取到地址。所以在add_value栈帧中,任意的操作传入的参数不会影响到main方法中的变量。
add_reference方法汇编:
- add_reference:
- pushl %ebp # 将上个栈帧的ebp地址压入当前栈帧(为了返回)
- movl %esp, %ebp # 将esp和ebp保持在同一个位置
- subl $16, %esp # 开辟16个字节大小的栈空间。
- movl 8(%ebp), %eax # 重点:将ebp+8位置的值移动到eax寄存器
- movl %eax, -4(%ebp) # 把eax寄存器存放的地址放入到ebp-4的位置。
- movl 12(%ebp), %eax # 重点:将ebp+12位置的值移动到eax寄存器
- movl %eax, -8(%ebp) # 把eax寄存器存放的地址放入到ebp-8的位置。
- movl -4(%ebp), %eax # 把ebp-4位置的值移动到eax寄存器中
- movl (%eax), %edx # eax寄存器解引用(获取到地址对应的真实值),并且放入到edx寄存器中。
- movl -8(%ebp), %eax # 把ebp-8位置的值移动到eax寄存器中。
- movl (%eax), %eax # eax寄存器解引用(获取到地址对应的真实值),并且放入到eax寄存器中。
- addl %edx, %eax # 将edx和eax寄存器的值相加
- popl %ebp # 将ebp的值弹出,这样就知道上一个栈帧的位置
- ret # 将eip的值弹出,这样就知道下一行执行的代码位置
注释特别特别的详细了。但是用注释+图片来理解更佳,所以看到下图。
在32位机中,方法的传参是通过栈的方法来传递。在32位机中,地址和int都是占用4个字节,所以在add_value开辟的栈帧中,直接用ebp寄存器+8和+12就可以获取到main方法中准备好的方法参数。但是在main方法中的汇编,可以看到使用了lea指令取到了int a和int b的地址。并且通过栈传递方法参数到add_reference方法中,所以在add_reference方法中,只要操作了这两个传入的地址参数就等同于在操作int a和int b变量。
一言以蔽之:
值传递:mov移动的真实值,等同于复制了一份真实值。所以操作复制的那一份,原本的不受影响
引用传递:通过lea指令获取到地址,再通过mov移动地址,也等同于复制了一份地址。但是地址最终是指向了真实值,所以后续操作复制的那一份地址也会影响到地址对应的真实值。
最后,如果本帖对您有一定的帮助,希望能点赞+关注+收藏!您的支持是给我最大的动力,后续会一直更新各种框架的使用和框架的源码解读~!