从汇编角度看C语言自增符号(++)


汇编语言几乎和可执行的机器语言相对应,所以如果想了解程序究竟在做什么,最好的办法就看汇编语言。这篇文章将从C语言编译出的汇编语言的层面,去看一看自增符号(++)的执行中,机器实际上发生了什么。

回顾自增符号

首先简单回顾一下C语言的自增符号,可以放在变量前或者变量后。
如果自增符号放在前面,表示变量先自增再使用,如果放在变量后面,表示先使用再自增。
举个例子,看下面两个程序:
程序1

#include <stdio.h>
int main(){
	int A=0, B=0;
	B = A++;
	return 0;
}

该程序结束的时候,A=1,B=0,因为A++是先使用后自增。
程序2

#include <stdio.h>
int main(){
	int A=0, B=0;
	B = ++A;
	return 0;
}

该程序结束的时候,A=1,B=1,因为++A是先自增后使用。
那么,如果是比较特殊的情况,B和A本身是同一个变量呢?
A = A++,或是A = ++A,结果会是多少?下文之后揭晓答案。

必要的汇编知识( AT&T 汇编格式)

针对新生朋友们已经做了简化,不系统讲解汇编语言知识,如果看不懂可以跳过直接看下一部分。本人汇编知识也很有限,如有讲解错误,请网友指出。

  1. 寄存器
    简单理解为计算机中一些特殊的位置,可以存储一定大小的数据。
    这里会涉及到的比较特殊的一个是rbp,大小为8字节,为当前堆栈指针(可以理解为指向程序要使用的一块空间),bp是"基址指针"(BASE POINTER)的意思,我们之后要通过这个寄存器间接访问栈空间,在栈空间上会保存我们计算用到的变量。
    具体如何访问呢?看下面的例子,通过这样的方式可以给变量分配空间或者读取空间中的变量值。

%rbp 表示 栈底的位置
-4(%rbp)表示 栈底的位置-4,即指向了一个可以存储4字节大小数据的新空间
-8(%rbp)表示 栈底的位置-8,即又指向了一个可以存储4字节大小数据的新空间

  1. mov
    mov表示把指定位置(可以是内存、寄存器等)的值传送到另一个位置。可以根据操作数的空间大小,在后面加上b(一字节)、w(两字节)、l(四字节)、q(八字节)。
    例如:

movl %esp %ebp 表示把寄存器%esp中的值放到寄存器ebp中。
movl %edx, -4(%rbp) 表示把寄存器%edx的值放到-4(%rbp)的位置上。
movl $0, %eax 表示把数值0放到寄存器eax中。

  1. lea
    lea根据括号里的源操作数来计算地址,然后把地址加载到目标寄存器中。同样,可以根据操作数的空间大小,在后面加上b(一字节)、w(两字节)、l(四字节)、q(八字节)。
    举个例子:leaq a(b, c, d), %rax 先计算地址a + b + c * d,然后把最终地址载到寄存器rax中。因此,看似lea本意是操作地址,但其实往往当做加法和乘法使用。
  2. add
    用法非常好理解,add A B就表示B=A+B。
    例如:addl $1 %eax就表示在%eax的值上自增1。

自增符号使用中发生的操作

以下汇编代码仅展示相关部分。

前自增赋值给自身

程序1

#include <stdio.h>
int main(){
	int A=10;
	A = ++A;
} 

汇编1

movl	$10, -4(%rbp)  // -4(%rbp) = 10 
addl	$1, -4(%rbp)   // -4(%rbp) += 1

可以看到,编译器直接把A = ++A简化成了A+=1,这是可以理解的,因为++A是在A的值上先自增然后再使用A的值,也就是把A+1的值赋给A。

前自增赋值给其他

程序2

#include <stdio.h>
int main(){
	int A=10,B=0;
	B = ++A;
} 

汇编2

movl	$10, -4(%rbp)   // -4(%rbp) = 10 
movl	$0, -8(%rbp)    // -8(%rbp) = 0
addl	$1, -4(%rbp)	// -4(%rbp) += 1
movl	-4(%rbp), %eax
movl	%eax, -8(%rbp)	// -8(%rbp) = %eax = -4(%rbp)

可以看到如果把++A赋给另一个值,和把++A赋给自己唯一多了一步就是赋值操作。在A自增完毕之后,把自增之后的值给B(对应第四行和第五行)。

后自增赋值给自身

程序3

#include <stdio.h>
int main(){
	int A=10;
	A = A++;
} 

汇编3

movl	$10, -4(%rbp)    // -4(%rbp) = 10
movl	-4(%rbp), %eax   // %eax = -4(%rbp) (记住%eax这里等于10,是一个临时存储变量)
leal	1(%rax), %edx    // %edx = 1+%rax   (自增过程,%rax是%eax的高位扩展,这里值相同)
movl	%edx, -4(%rbp)   // -4(%rbp) = %edx (把自增后的值赋给自己,即为11)
movl	%eax, -4(%rbp)   // -4(%rbp) = %eax (把自增前的值赋给等号左侧的变量,恰好也是自己,值为10)

所以最后 A = 10 ,虽然是先计算了自增,但可以看到赋给其他变量(这里恰好是自身)的值是增加前的值。所以,A++虽然会在A的值上自增1,但这个表达式返回的是原始的A值,如果A = A++相当于不改变A的值。

后自增赋值给其他

程序4

#include <stdio.h>
int main(){
	int A=10,B=0;
	B = A++;
} 

汇编4

movl	$10, -4(%rbp)  // -4(%rbp) = 10
movl	$0, -8(%rbp)   // -8(%rbp) = 0
movl	-4(%rbp), %eax // %eax = -4(%rbp) (记住%eax这里等于10)
leal	1(%rax), %edx  // %edx = 1+%rax   (自增过程,%rax是%eax的高位扩展,这里值相同)
movl	%edx, -4(%rbp) // -4(%rbp) = %edx (把自增后的值赋给自己)
movl	%eax, -8(%rbp) // -8(%rbp) = %eax (把自增前的值赋给等号左侧的变量)

拿程序4和程序3对比,唯一区别就是把A++的值赋给了一个新的变量。可以看到,过程依然为A首先自增1(对应第四行和第五行),然后返回A之前的值,赋给另一个值B(对应第六行)。

总结

由此我们可以至少看出以下几点结论:

  1. 前自增和后自增的主要差别在于要返回一个值给其他变量时,如果不返回值给其他变量,那两种自增没有区别。

  2. 前自增就是先把操作数+1,然后把当前操作数的值返回出来使用,过程比较简单,全程只设计到一个寄存器的使用(不含基址指针rbp的话)。

  3. 后自增需要先把操作数的值放到一个寄存器上,再把操作数+1的值放到另一个寄存器上,然后分别把+1的值赋给自身,把原始的值返回使用,并且返回使用的步骤在赋值给自身之后,全程要用到两个寄存器。

    这也就完美解释了,为什么A=A++不改变A的值,因为先把自增的值赋给A,这个时候A是加1的,然后把A的原始值拿来使用,此处的使用即为把A的值赋给A,这时候A的值又变回了原来的值,中间增加以及赋值给自身的两步都被覆盖掉了。

自增运算符的使用还有更多复杂情况,但都离不开这几种基本情况,所以只要搞清在汇编层面发生了什么,就可以深入理解C语言的自增运算符,正确判断任何情况下的结果。
这也体现了学习一些基本的汇编知识的重要性。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛钦亮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值