循环控制语句的处理
上面说的都是顺序流程,那么现在就让我们分析一下循环流程的处理,看一下 for 循环
以及 if 条件分支
等 c 语言程序的 流程控制
是如何实现的,我们还是以代码以及编译后的结果为例,看一下程序控制流程的处理过程。
// 定义MySub 函数 void MySub(){ // 不做任何处理 } // 定义MyFunc 函数 void Myfunc(){ int i; for(int i = 0;i < 10;i++){ // 重复调用MySub十次 MySub(); } }
上述代码将局部变量 i 作为循环条件,循环调用十次MySub
函数,下面是它主要的汇编代码
xor ebx, ebx ; 将寄存器清0 @4 call _MySub ; 调用MySub函数 inc ebx ; ebx寄存器的值 + 1 cmp ebx,10 ; 将ebx寄存器的值和10进行比较 jl short @4 ; 如果小于10就跳转到 @4
C 语言中的 for 语句是通过在括号中指定循环计数器的初始值(i = 0)、循环的继续条件(i < 10)、循环计数器的更新(i++) 这三种形式来进行循环处理的。与此相对的汇编代码就是通过比较指令(cmp)
和 跳转指令(jl)
来实现的。
下面我们来对上述代码进行说明
MyFunc
函数中用到的局部变量只有 i ,变量 i 申请分配了 ebx 寄存器的内存空间。for 语句括号中的 i = 0 被转换为 xor ebx,ebx
这一处理,xor 指令会对左起第一个操作数和右起第二个操作数进行 XOR 运算,然后把结果存储在第一个操作数中。由于这里把第一个操作数和第二个操作数都指定为了 ebx,因此就变成了对相同数值的 XOR 运算。也就是说不管当前寄存器的值是什么,最终的结果都是0。类似的,我们使用 mov ebx,0
也能得到相同的结果,但是 xor 指令的处理速度更快,而且编译器也会启动最优化功能。
XOR 指的就是异或操作,它的运算规则是 如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。
相同数值进行 XOR 运算,运算结果为0。XOR 的运算规则是,值不同时结果为1,值相同时结果为0。例如 01010101 和 01010101 进行运算,就会分别对各个数字位进行 XOR 运算。因为每个数字位都相同,所以运算结果为0。
ebx 寄存器的值初始化后,会通过 call 指定调用 _MySub 函数,从 _MySub 函数返回后,会执行inc ebx
指令,对 ebx 的值进行 + 1 操作,这个操作就相当于 i++ 的意思,++ 表示的就是当前数值 + 1。
这里需要知道 i++ 和 ++i 的区别
i++ 是先赋值,复制完成后再对 i执行 + 1 操作
++i 是先进行 +1 操作,完成后再进行赋值
inc
下一行的 cmp
是用来对第一个操作数和第二个操作数的数值进行比较的指令。cmp ebx,10
就相当于 C 语言中的 i < 10 这一处理,意思是把 ebx 寄存器的值与10进行比较。汇编语言中比较指令的结果,会存储在 CPU 的标志寄存器中。不过,标志寄存器的值,程序是无法直接参考的。那如何判断比较结果呢?
汇编语言中有多个跳转指令
,这些跳转指令会根据标志寄存器的值来判断是否进行跳转操作,例如最后一行的 jl,它会根据 cmp ebx,10 指令所存储在标志寄存器中的值来判断是否跳转,jl
这条指令表示的就是 jump on less than(小于的话就跳转)
。发现如果 i 比 10 小,就会跳转到 @4 所在的指令处继续执行。
那么汇编代码的意思也可以用 C 语言来改写一下,加深理解
i ^= i; L4: MySub(); i++; if(i < 10) goto L4;
代码第一行 i ^= i 指的就是 i 和 i 进行异或运算,也就是 XOR 运算,MySub() 函数用 L4 标签来替代,然后进行 i 自增操作,如果i 的值小于 10 的话,就会一直循环 MySub() 函数。
条件分支的处理方法
条件分支的处理方式和循环的处理方式很相似,使用的也是 cmp 指令和跳转指令。下面是用 C 语言编写的条件分支的代码
// 定义MySub1 函数 void MySub1(){ // 不做任何处理 } // 定义MySub2 函数 void MySub2(){ // 不做任何处理 } // 定义MySub3 函数 void MySub3(){ // 不做任何处理 } // 定义MyFunc 函数 void MyFunc(){ int a = 123; // 根据条件调用不同的函数 if(a > 100){ MySub1(); } else if(a < 50){ MySub2(); } else { MySub3(); } }
很简单的一个实现了条件判断的 C 语言代码,那么我们把它用 Borland C++ 编译之后的结果如下
_MyFunc proc near push ebp mov ebp,esp mov eax,123 ; 把123存入 eax 寄存器中 cmp eax,100 ; 把 eax 寄存器的值同100进行比较 jle short @8 ; 比100小时,跳转到@8标签 call _MySub1 ; 调用MySub1函数 jmp short @11 ; 跳转到@11标签 @8: cmp eax,50 ; 把 eax 寄存器的值同50进行比较 jge short @10 ; 比50大时,跳转到@10标签 call _MySub2 ; 调用MySub2函数 jmp short @11 ; 跳转到@11标签 @10: call _MySub3 ; 调用MySub3函数 @11: pop ebp ret _MyFunc endp
上面代码用到了三种跳转指令,分别是jle(jump on less or equal)
比较结果小时跳转,jge(jump on greater or equal)
比较结果大时跳转,还有不管结果怎样都会进行跳转的jmp
,在这些跳转指令之前还有用来比较的指令 cmp
,构成了上述汇编代码的主要逻辑形式。
了解程序运行逻辑的必要性
通过对上述汇编代码和 C 语言源代码进行比较,想必大家对程序的运行方式有了新的理解,而且,从汇编源代码中获取的知识,也有助于了解 Java 等高级语言的特性,比如 Java 中就有 native 关键字修饰的变量,那么这个变量的底层就是使用 C 语言编写的,还有一些 Java 中的语法糖只有通过汇编代码才能知道其运行逻辑。在某些情况下,对于查找 bug 的原因也是有帮助的。
上面我们了解到的编程方式都是串行处理的,那么串行处理有什么特点呢?
串行处理最大的一个特点就是专心只做一件事情
,一件事情做完之后才会去做另外一件事情。
计算机是支持多线程的,多线程的核心就是 CPU切换,如下图所示
我们还是举个实际的例子,让我们来看一段代码
// 定义全局变量 int counter = 100; // 定义MyFunc1() void MyFunc(){ counter *= 2; } // 定义MyFunc2() void MyFunc2(){ counter *= 2; }
上述代码是更新 counter 的值的 C 语言程序,MyFunc1() 和 MyFunc2() 的处理内容都是把 counter 的值扩大至原来的二倍,然后再把 counter 的值赋值给 counter 。这里,我们假设使用多线程处理
,同时调用了一次MyFunc1 和 MyFunc2 函数,这时,全局变量 counter 的值,理应变成 100 * 2 * 2 = 400。如果你开启了多个线程的话,你会发现 counter 的数值有时也是 200,对于为什么出现这种情况,如果你不了解程序的运行方式,是很难找到原因的。
我们将上面的代码转换成汇编语言的代码如下
mov eax,dword ptr[_counter] ; 将 counter 的值读入 eax 寄存器 add eax,eax ; 将 eax 寄存器的值扩大2倍。 mov dword ptr[_counter],eax ; 将 eax 寄存器的值存入 counter 中。
在多线程程序中,用汇编语言表示的代码每运行一行,处理都有可能切换到其他线程中。因而,假设 MyFun1 函数在读出 counter 数值100后,还未来得及将它的二倍值200写入 counter 时,正巧 MyFun2 函数读出了 counter 的值100,那么结果就将变为 200 。
为了避免该bug,我们可以采用以函数或 C 语言代码的行为单位来禁止线程切换的锁定
方法,或者使用某种线程安全的方式来避免该问题的出现。
现在基本上没有人用汇编语言来编写程序了,因为 C、Java等高级语言的效率要比汇编语言快很多。不过,汇编语言的经验还是很重要的,通过借助汇编语言,我们可以更好的了解计算机运行机制。