顺序执行
首先当然是最简单的顺序执行了,来一个简单的例子。
现在有不知道多少个计算题(小学生时候都做过吧。)
10+15=
79+13=
21+12=
56-8=
26-1=
34-3=
56-2=
=================
作为碳基生物哦,我们是可以直接一道题一道题做下来的。写完第一个写第二个对吧,但是计算机,cpu这种硅基生命(存疑),是没这种功能的,需要一个东西去专门记录做到了哪一道题,否则他就不知道接下来去做哪一个了。
这个时候就是需要一个神奇的寄存器了,专门用来存放“程序执行到哪里了”的这么一个寄存器,eip寄存器。这个寄存器有一个特殊之处是,无法通过mov指令进行修改。
eip本质上的作用是,当在执行一条指令的时候,eip此时存储的是下一条将要执行的指令的地址。
if else
我们在c程序中,写一条if else这种条件语句,其实是个很ez的事情。但是到了汇编就不一样了,其实也很ez,只不过不熟悉罢了。
在汇编中,就是通过jmp指令,(其实c中的goto关键字反汇编过来就是jmp指令)。
它的功能就是直接跳转到某个地方,你可以往前跳转也可以往后跳转,跳转的目标就是jmp后面的标签,这个标签在经过编译之后,会被处理成一个地址,实际上就是在往某个地址处跳转,而jmp在CPU内部发生的作用就是修改eip,让它突然变成另外一个值,然后CPU就乖乖地跳转过去执行别的地方的代码了。
而if是怎么实现的呢?
global main
main:
mov eax, 66
cmp eax, 10 ; 对eax和10进行比较
jle mdzz ; 小于或等于的时候跳转
sub eax, 10
mdzz:
ret
状态寄存器
到这里,有一个问题出现了,在汇编语言里面实现“先比较,后跳转”的功能时,后面的跳转指令是怎么利用前面的比较结果的呢?
这就涉及到另一个寄存器了。在此之前,先想一下,如果自己在脑子里思考同样的逻辑,是怎么样的?
- 先比较两个数
- 记住比较结果
- 根据比较结果作出决定
好了,这里又来了一个“记住”的动作了。CPU里面也有一个专用的寄存器,用来专门“记住”这个cmp指令的比较结果的,而且,不仅是cmp指令,它还会自动记住其它一些指令的结果。这个寄存器就是:
eflags
名为“标志寄存器”,它的作用就是记住一些特殊的CPU状态,比如前一次运算的结果是正还是负、计算过程有没有发生进位、计算结果是不是零等信息,而后续的跳转指令,就是根据eflags寄存器中的状态,来决定是否要进行跳转的。
cmp指令实际上是在对两个操作数进行减法,减法后的一些状态最终就会反映到eflags寄存器中。
循环结构
上回说到C语言中if这样的结构,在汇编里对应的是怎么回事,实质上,这就是分支结构的程序在汇编里的表现形式。
实际上,循环结构相比分支结构,本质上,没有多少变化,仅仅是比较合跳转指令的组合的方式与顺序有所不同,所以形成了循环。
当然,这个说法可能稍微拗口了一点。说得简单一点,循环的一个关键特点就是:
- 程序在往回跳转
细细想,好像有道理哦,如果程序每到一个位置就往前跳转,那就是死循环,如果是在这个位置根据条件决定是否要向前跳转,那就是有条件的循环了。
口说无凭,还是先来分析一下一个C语言的while循环:
(Talk is cheap, show me the code!)
int sum = 0;
int i = 1;
while( i <= 10 ) {
sum = sum + i;
i = i + 1;
}
想必这段程序多数人都非常熟悉了,当年自己第一次学习循环的时候就碰到这个题目,脑子短路了,心里总想着这不就是一个等差数列公式么,题目却强行出现在循环一章的后面,最后结果让人大跌眼睛,这是要我老老实实像SHAB一样去加啊。
跑题了,先大致总结一下这个程序的关键部分到底在干什么:
- \1. 比较i和10的大小
- \2. 如果i <= 10则执行代码块,并回到(1)
- \3. 如果不满足 i <= 10,则跳过代码块
好了,按照这个逻辑,在C语言中不使用循环怎么实现?其实也非常简单:
int sum = 10;
int i = 1;
_start:
if( i <= 10 ) {
sum = sum + i;
i = i + 1;
goto _start;
}
这还不够,我们还得做一次变形,为什么呢?回想一下前面说的分之程序在汇编里的情况:
if ( a > 10 ) {
// some code
}
上述C代码,暂且成为“正宗C代码”,等价的汇编大致结构如下:
cmp eax, 10
jle out_of_block
; some code
out_of_block:
再等价变换回C语言,这里把这种风格叫做“山寨C代码”,实际上就是这样的:
if( a <= 10 ) goto out_of_block;
// some code
out_of_block:
经过比较,我们可以发现“山寨C代码”和“正宗C代码”之间的一些区别:
- 山寨版中,if块里只需要放一条跳转语句即可
- 山寨版中,if里的条件是反过来的
- 山寨版中,跳转语句的功能是跳过“正宗C代码”的if块
相当于是:不满足条件就跳过if中的语句块。
那循环呢?咱们把循环的C等价代码做一次变换,也就是把只含有goto和if的“正宗C代码”变换为“山寨C代码”的形式:
int sum = 10;
int i = 1;
_start:
if( i > 10 ) {
goto _end_of_block;
}
sum = sum + i;
i = i + 1;
goto _start;
_end_of_block:
大致看一下流程,再对比源代码:
int sum = 0;
int i = 1;
while( i <= 10 ) {
sum = sum + i;
i = i + 1;
}
自己在脑子里面模拟一遍,是不是就能发现什么了?这俩货分明就是一个东西,执行的顺序和过程完全就是一样的。
到这里,我们的循环结构,全都被拆散成了最基本的结构,这种结构有一个关键的特点:
- 所有if块中都仅有一条goto语句,别的啥都没了
到这里,本段就到位了。
用汇编写出循环
前面已经介绍了“如何把一个循环拆解成只有if和goto的结构”,有了这个结构之后,其实要写出汇编就非常容易了。
继续看山寨版的循环:
int sum = 10;
int i = 1;
_start:
if( i > 10 ) {
goto _end_of_block;
}
sum = sum + i;
i = i + 1;
goto _start;
_end_of_block:
其实,稍微仔细一点就能发现,把这玩意儿写成汇编,就是逐行翻译就完事儿了。动手:
global main
main:
mov eax, 0
mov ebx, 1
_start:
cmp ebx, 10
jg _end_of_block
add eax, ebx
add ebx, 1
jmp _start
_end_of_block:
ret
这里面其实有一个套路:
- 单条goto语句可以直接用jmp语句替代
- if和goto组合的语句块可以用cmp和j*指令的组合替代
最后,其它语句该干啥干啥。
这?竟然?就?用汇编?写出?循环?来了?
嗯,是的。不需要任何一个新的指令,全都是前面提及过的基本指令,只是套路不一样了而已。
其实这就是一个套路,稍微总结一下就能发现,一个将while循环变换为汇编的过程如下:
- 将while循环拆解成只有if和goto的形式
- 将if形式的语句拆解成if块中仅有一行goto语句的形式
- 从前往后逐行翻译成汇编语言
其它循环呢?
那while循环能够搞定了,其它类型的呢?do-while循环、for循环呢?
其实,在C语言中,这三种循环之间都是可以相互变换的,也就是说for循环可以变形成为while循环,while循环也可以变成for循环。举个例子:
int i = 1;
int sum = 0;
for(i = 0; i <= 10; i ++) {
sum = sum + i;
}
int sum = 0;
int i = 1;
while( i <= 10 ) {
sum = sum + i;
i = i + 1;
}
上述两个片段的代码,其实就是等价的,仅仅是形式不同。只是有的循环思路用for循环写出来好看一些,有的思路用while循环写出来好看一些,别的没什么本质区别,经过编译器一倒腾之后,就更没有任何区别了。