[MUSIC]
下面我们来看一下循环结构的机器级表示。
在c语言程序当中,循环结构的语句可以
是do-while语句,while语句,和for循环语句。
do-while语句的语法是这样子的。
对应的指令序列,我们可以写成一个loop这个标号。
然后在这个地方,实现这个循环体的功能,用若干条指令
执行循环体,然后再用若干条指令计算条件表达式。
然后再根据这个条件表达式是否满足结果为true。
然后决定是继续执行loop处的这个过程体还是跳出这个过程体。
这个实际上就是do-while语句。
while的循环,语法是这样子的,就是当条件表达式满足的时候,就一直做。
所以它对应的指令序列,应该是先计算这个条件表达式的值,
然后呢判断这个值是真还是假,如果假,不满足的话就跳出循环。
满足呢,继续执行循环体。
然后在循环体里面再计算新的条件表达式的值,满足条件的话继续循环。
然后再去计算新的条件表达的值,再去继续循环,一直到不满足为止,跳出循环。
for循环,语法是这样子的,有一个开始 表达式,有一个最终结束的条件表达式。
然后呢还要进行修改循环变量的一个表达式。
在这个表达式成立的时候就一直循环。
然后再去修改,再去判断,再去满足条件,再执行循环,一直
是这样,因此for循环对应的指令序列应该是这样子的。
首先,对开始的循环变量计算开始的初值。
然后呢计算结束的条件表达式。
如果不满足条件就跳出循环,满足条件 就去执行循环体里面的若干条指令。
然后呢再去更新循环变量的值。
根据更新的值再来计算条件表达式。
再看是否满足条件,是的话继续循环。
所以for循环对应的整个的指令序列应该是这样的。
在这些所有的循环结构的语句当中。
对应的这些指令序列里面都有相应的条件转移指令。
红颜色的这个地方都是条件转移指令。
我们后面举个例子来说明。
前面我们曾经举过一个递归函数的例子。
就是对01234这样子的一个等差数列进行求和。
这样的一个功能,我们用了递归来实现。
显然这个功能我们是可以直接用一个求和公式来算的。
但是我们为了说明递归函数的原理,找了最简单的递归过程来说明的。
所以我们这个递归函数仅仅是为了说明原理而引入的,真正实现的时候我们肯定不会用递- 归函数。
这是我们前面讲过程调用的时候举的一个递归调用的例子。
这里面我们讲循环结构,所以我们也用这个求和的例子,用循环实现。
然后比较用循环结构语句实现的这种方式和用递归实现的这个方式
它们之间的差别到底在哪里,这个循环结构来实现的这个求和
它的对应的指定序列在这个函数体当中是这样的一串指令。
那么显然这边的入口参数,n呢,它一定是ebp加8那个位子。
然后这里面有2个局部变量i和result。
通常这些局部变量应该是分配在栈里面的。
因为这个地方的局部变量i和result都是最简单的简单变量。
这些简单变量编译器通常是直接分配在寄存器里面,
而不分配在栈里面,如果这个变量是一个复杂类型的变量,
通常是在栈里面,这里面因为是简单变量, 我们就直接把它分配在寄存器里面。
这边的i我们假定是分配在 edx里面,result分配在eax里面,也就是说
入口参数n是在ebp加8的那个内存单元里面。
i呢是分配在edx里面。
result分配在eax里面,就这个i一开始是等于1。
所以把1呢送到edx寄存器里面。
0送到eax寄存器里面。
这个for循环先要去计算结束的这个表达式 是否满足,满足的话直接就跳出循环就不做了。
所以这个结束的条件是i小于等于n。
因此我们把edx的内容,就是i ecx的内容,就是这边的n,进行比较。
如果大于的话,也就是i大于n的话,就跳 出循环,因此我们就跳出到l2。
这边的g表示greater,就是大于。
而且这个大于是带符号整数比较。
这个i和n都是按带符号整数来比的。
如果是无符号整数的话就是ga above,这应该是带符号,因为n是int型的。
i也是int型的,所有的这些变量都是int型的,所以用带符号比较。
小于等于的时候就进行循环体里面的工作。
这个是比较这个,循环体里面的语句是 这样,就是i和result相加,再送到result。
因此把eax里面的result和edx里面的i相加。
加出来的结果还是放到result里面去,result是一个返回的值。
所以是在eax这个寄存器里面的。
然后呢去修改循环变量,i要加1,edx里面加1。
最后再去比较这个结果,这个比较是用比较并转移2条指令实现的。
这个就是i和n比,如果是小于等于的话继续循环。
所以这个还是很简单,这个里面我们可以看到
过程体里面没有用到被调用过程保存寄存器。
也就是没有用到ebx esi edi这三个寄存器。
用的是eax ecx和edx这三个寄存器,其实我们可以总结一下,到目前为止
我们举的所有的例子,基本上都是用这三个寄存器,不够用的时候才会用到ebx。
如果我们用了ebx,那在这个里面一开始就要先要保存ebx的值。
这没有用到,所以我们不需要保存。
在这个过程的栈帧当中,只要保存这个ebp,ebp是每个栈帧的底部。
所以在这个里面仅仅是保留了这个寄存器的内容,只占4个字节。
如果考虑这个每个栈帧,至少是16个字节,也就是每个栈帧都按16字节对齐的话。
那么这个循环实现的过程当中,栈帧也只有16个字节。
而递归用了多少字节呢,递归用了16乘n的字节。
是n倍的关系,我们来看一下,前面我们讲的这个递归方式。
递归方式,每递归一次,都会在栈里面长 出一个栈帧,而这个栈帧占了16个字节。
44 16,一共递归n次。
所以它的这个自己数就是16个字节乘n。
因此长出来的栈帧最大,会长16乘n个字节。
所以是16倍的关系,同时这个递归过程,它的时间开销也非常大。
每递归一次,都要执行这16条指令。
所以整个的这个时间开销就是16乘n条指令。
可以看出递归调用执行的这个指令条数
和所用的栈空间,都比循环方式
多得多,所以为了提高程序的性能,能够用非递归方式
最好用非递归方式,递归很简单,用高级语言写的时候很简单。
但是,这个高级语言转换出来的目标代码, 它的执行过程是非常复杂的,一次一次地递归地去执行这个过程。
它地指令条数,以及一次一次递归生成的这个栈帧,这个空间,都
会开销很大,这个递归函数和循环结构的比较。
最后我们来看一个逆向工程,所谓逆向工程就是说,我们
可以通过机器级代码,就是通过这个指令序列
来推出高级语言这个语句是什么样子的。
从低级的机器级的程序推高级语言程序,这个叫逆向工程。
这个里面我们看到这个函数是有一个参数。
这个参数当然是在ebp加8那个位子。
然后这个函数里面我们可以猜出来它是有
1个result,因为它是一个函数,有返回值的。
所以这个返回值,result一开始赋的是等于0,因为
这个返回值总是在eax里面,所以我们可以猜出返回的结果。
然后我们看出这是一个循环。
所以它一定有1个循环变量i。
这个i的初值是等于0,这个循环 最终我们看ecx总是在最后会加1。
因此我们可以看出这个地方的循环变量实现的是
i++,转入这个循环之前会进行比较。
比较的时候是把ecx里面的这个i和32比。
不相等的时候才转进去,所以我们可以看出这个循环结束的条件
是相等,不相等继续循环,相等那就是跳出循环。
这5条指令对应的实际上就是标号为4的这个地方的一串语句。
那么这串语句的功能是什么呢,我们可以分析一下,刚才我们已经分析了
1处是i等于0,2处呢是i不等于32,就是 循环继续循环的条件是i不等于32。
3这个地方是i++,那4这个地方对应的这5条指令 根据这条指令的功能,它是把result
乘2,就是加起来,2个result加起来就相当于乘2。
放到edx里面去,乘2就相当于左移1位。
然后edx放在这以后再去执行这条指令的时候,是把ebx也就加的x
送到eax,再和1相与,所以这2条语句实现的就是 x和1相与,这条语句是把
相语的结果和刚才左移的结果相加。
加出来的结果在eax里面,因此这4条语句实现了
result等于result左移2位,就是乘2。
或上,这个or表示或,或上x和1相与。
这个结果放到result里面,eax result,下面的这条指令
实际上是对x进行右移,右移1位。
就是把x逻辑右移,为什么x逻辑右移呢,我们可以看出它的类型是unsigned,是无- 符号整数。
无符号整数采用的右移方式一定是 逻辑右移,也就是高位是补0,而不是高位补符。
这条指令对应的语句应该是对x 右移1位再送给x,因此4这个地方
有2条语句,1条是对result进行复制的语句。
1条是对x进行右移以后再附给x的1条语句,这就是1个逆向工程的例子。
[MUSIC]
[MUSIC]