循环语句(for)是计算机编程中的一种基本控制结构,它允许程序按照指定的次数或范围重复执行一段代码块。for循环在处理需要进行迭代操作的情况下非常有用,它使得程序可以更加方便地控制循环的次数。
一般来说,for循环由三个部分组成:初始化部分、条件表达式和更新部分,以及一个需要重复执行的代码块。在每次循环迭代开始时,程序首先执行初始化部分,然后检查条件表达式的值,如果为真,则执行代码块,并在每次循环结束后执行更新部分。只要条件表达式为真,for循环就会一直重复执行;一旦条件表达式为假,循环将停止,程序继续执行循环之后的代码。
11.14 FOR 循环结构优化 For语句先初始化条件变量,然后在判断是否符合条件,符合则执行循环体,不符合则跳过执行。For循环结构的效率最低,该语句的构建往往需要三个跳转来实现,首先需要初始化变量此处要进行一次判断,其次是内部循环体需要另一个判断通常用于实现跳出循环体,最后一步则需要一个无条件跳转指令跳回到循环首地址,但在开启了O2优化时编译器也会尽可能将其转换为While语句,如果可以还会继续将While转为带有IF语句的Do循环来提高执行效率。
.386p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc includelib kernel32.lib .data count DWORD ? .code main PROC mov dword ptr ds:[count],0 ; 设置 int x = 0; jmp L2 L1: mov eax,dword ptr ds:[count] ; x = x++ add eax,1 mov dword ptr ds:[count],eax L2: cmp dword ptr ds:[count],10 ; 比较 x < 10 jge lop_end xor eax,eax ; 执行循环体 jmp L1 lop_end: int 3 invoke ExitProcess,0 main ENDP END main
虽然For语句在执行效率上来说是最低的,但该语句的使用确是最符合我们思维方式的,在高级语言中应用最为广泛,例如在Python中For循环体被简化成了for x in range(2,10)
它可以指定一个循环范围,该语句利用汇编完全也可以被构建出来,我们接着尝试构建一下这个特别的循环体。
.386p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc includelib kernel32.lib .data start_count DWORD ? end_count DWORD ? .code main PROC mov dword ptr ds:[start_count],2 ; 指定开始循环编号 mov dword ptr ds:[end_count],5 ; 指定结束循环编号 mov ecx,dword ptr ds:[start_count] L1: cmp dword ptr ds:[end_count],ecx jle lop_end xor eax,eax ; 循环体内部 add ecx,1 ; 每次递增 mov dword ptr ds:[start_count],ecx jmp L1 lop_end: int 3 invoke ExitProcess,0 main ENDP END main
11.20 仿写For水仙花数 该C++代码实现了水仙花数的查找算法,水仙花数是指一个三位数,它的每个位上的数字的立方和等于它本身。在循环中,遍历100~999
之间的每一个数,将其分解为三个数(百、十、个位),再将三个数分别平方并相加,判断与原数是否相等,如果相等则输出该数即为水仙花数。
例如: 153是一个水仙花数,因为153等于1的3次方加上5的3次方加上3的3次方
#include <stdio.h> #include <Windows.h> int main (int argc, char *argv[]) { int x, y, z, n; for (n = 100 ; n < 1000 ; n++) { x = n / 100 ; y = n / 10 % 10 ; z = n % 10 ; if (x * 100 + y * 10 + z == x*x*x + y*y*y + z*z*z) { printf ("水仙花: %-5d \n" , n); } } system("pause" ); return 0 ; }
尝试使用汇编实现计算逻辑,这段代码没有任何难度,因为并不会涉及到嵌套循环的问题,只是在计算四则运算时需要格外注意些。
.386p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc includelib kernel32.lib include msvcrt.inc includelib msvcrt.lib .data x DWORD ? y DWORD ? z DWORD ? n DWORD ? szFmt BYTE '水仙花: %-5d ',0dh,0ah,0 .code main PROC mov dword ptr ds:[n],100 ; n = 100 jmp L1 L2: mov eax,dword ptr ds:[n] add eax,1 ; n++ mov dword ptr ds:[n],eax L1: mov eax,dword ptr ds:[n] cmp eax,1000 ; n < 1000 jge lop_end mov eax,dword ptr ds:[n] cdq mov ecx,100 ; x = n / 100; idiv ecx mov dword ptr ds:[x],eax mov eax,dword ptr ds:[n] cdq mov ecx,10 idiv ecx ; y = n / 10; cdq mov ecx,10 idiv ecx ; y = y % 10; mov dword ptr ds:[y],edx mov eax,dword ptr ds:[n] cdq mov ecx,10 idiv ecx ; z = n % 10; mov dword ptr ds:[z],edx ; 开始执行if()比较语句 imul eax,dword ptr ds:[x],100 ; x * 100 imul ecx,dword ptr ds:[y],10 ; y * 10 add eax,dword ptr ds:[z] ; + z add ecx,eax mov edx,dword ptr ds:[x] imul edx,dword ptr ds:[x] ; x*x*x imul edx,dword ptr ds:[x] mov eax,dword ptr ds:[y] imul eax,dword ptr ds:[y] ; y*y*y imul eax,dword ptr ds:[y] add edx,eax mov eax,dword ptr ds:[z] imul eax,dword ptr ds:[z] ; z*z*z imul eax,dword ptr ds:[z] add edx,eax cmp ecx,edx ; (x * 100 + y * 10 + z) == (x*x*x + y*y*y + z*z*z) jne L2 mov eax,dword ptr ds:[n] invoke crt_printf,addr szFmt,eax jmp L2 lop_end: int 3 main ENDP END main
11.21 For循环尝试判断 该C++代码实现了一个简单的循环,遍历数组中的所有元素并输出大于等于50的元素。在循环中,通过判断Array数组中每个元素与50的大小关系,如果元素大于等于50,则使用printf函数输出该元素的值。最终程序输出所有大于等于50的元素。
#include <stdio.h> #include <Windows.h> int main (int argc,char *argv[]) { int Array[10 ] = { 56 ,78 ,33 ,45 ,78 ,90 ,32 ,44 ,56 ,67 }; for (int x = 0 ; x < 10 ; x++) { if (Array[x] >= 50 ) { printf ("out -> %d \n" , Array[x]); } } return 0 ; }
上述C语言代码如果通过汇编语言实现可以写成如下样子,读者可自行理解流程;
.386p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc includelib kernel32.lib include msvcrt.inc includelib msvcrt.lib .data MyArray DWORD 56,78,33,45,78,90,32,44,56,67 count DWORD ? szFmt BYTE 'out -> %d ',0dh,0ah,0 .code main PROC mov dword ptr ds:[count],0 ; int x = 0 jmp L1 L2: mov eax,dword ptr ds:[count] add eax,1 ; x ++ mov dword ptr ds:[count],eax L1: cmp dword ptr ds:[count],10 ; x < 10 jge lop_end mov eax,dword ptr ds:[count] ; 获取循环次数,当作因子 lea esi,dword ptr ds:[MyArray] ; 取数组基地址 mov ebx,dword ptr ds:[esi + eax * 4] ; 因子寻址 cmp ebx,50 jl L2 ; 如果小于50则跳转到下一次循环 invoke crt_printf,addr szFmt,ebx ; 调用系统crt jmp L2 lop_end: int 3 invoke ExitProcess,0 main ENDP END main
在读者学会了上述代码编写之后,我们继续增加代码的复杂度,如下所示代码实现了对整型数组的最大值、最小值、元素总和以及平均值的计算。在循环中,通过依次遍历数组中的每一个元素,维护一个当前最大值max_result
和最小值min_result
,并对元素进行累加求和,最终计算出数组中所有元素的平均值avg_result
。代码中使用printf
函数输出求得的四个值(max、min、sum、avg)
,并使用system
函数暂停程序以便观察输出结果。
#include <stdio.h> #include <Windows.h> int main (int argc, char *argv[]) { int Array[10 ] = { 56 ,78 ,33 ,45 ,78 ,90 ,32 ,44 ,56 ,67 }; int max_result = 0 ,min_result = 100 ,sum_result = 0 ,avg_result = 0 ; for (int x = 0 ; x < 10 ; x++) { if (Array[x] >= max_result) { max_result = Array[x]; } if (Array[x] <= min_result) { min_result = Array[x]; } sum_result = sum_result + Array[x]; avg_result = sum_result / 10 ; } printf ("max = %d min = %d sum = %d avg = %d \n" , max_result,min_result,sum_result,avg_result); system("pause" ); return 0 ; }
上述代码读者可尝试使用汇编语言来实现一下,如下代码是笔者思考后编写出来的实现流程,读者可自行对照参考;
.386p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc includelib kernel32.lib include msvcrt.inc includelib msvcrt.lib .data MyArray DWORD 56,78,33,45,78,90,32,44,56,67 count DWORD ? max_result DWORD 0 min_result DWORD 100 sum_result DWORD 0 avg_result DWORD 0 szFmt BYTE 'max = %d min= %d sum= %d avg = %d ',0dh,0ah,0 .code main PROC mov dword ptr ds:[count],0 ; int x = 0 jmp L1 L2: mov eax,dword ptr ds:[count] add eax,1 ; x ++ mov dword ptr ds:[count],eax L1: cmp dword ptr ds:[count],10 ; x < 10 jge lop_end mov eax,dword ptr ds:[count] lea esi,dword ptr ds:[MyArray] mov ebx,dword ptr ds:[esi + eax * 4] cmp ebx,dword ptr ds:[max_result] ; Array[x] >= max_result jl L3 mov dword ptr ds:[max_result],ebx ; max_result = Array[x]; L3: mov ebx,dword ptr ds:[esi + eax * 4] cmp ebx,dword ptr ds:[min_result] ; Array[x] <= min_result jg L4 mov dword ptr ds:[min_result],ebx L4: mov ebx,dword ptr ds:[esi + eax * 4] ; Array[x] add dword ptr ds:[sum_result],ebx ; sum_result = sum_result + Array[x]; mov eax,dword ptr ds:[sum_result] cdq ; 符号扩展 mov ecx,10 ; / 10 idiv ecx ; sum_result / 10; mov dword ptr ds:[avg_result],eax ; avg_result jmp L2 lop_end: mov eax,dword ptr ds:[max_result] mov ebx,dword ptr ds:[min_result] mov ecx,dword ptr ds:[sum_result] mov edx,dword ptr ds:[avg_result] invoke crt_printf,addr szFmt,eax,ebx,ecx,edx int 3 main ENDP END main
11.22 For循环多重IF判断 该C++代码实现了对两个数组进行元素相加,并输出相加结果的奇偶性。在循环中,对SrcArray
和DstArray
两个数组中的元素相加,如果两个元素均不为0,则判断相加的结果是否为偶数,如果是,则使用printf函数输出偶数sum的形式,否则输出基数sum的形式。其中sum表示两个元素相加的结果。代码中使用system函数暂停程序以便观察输出结果。
#include <stdio.h> #include <Windows.h> int main (int argc, char *argv[]) { int SrcArray[10 ] = { 56 ,78 ,33 ,45 ,78 ,90 ,32 ,15 ,56 ,67 }; int DstArray[10 ] = { 59 ,77 ,89 ,23 ,11 ,45 ,67 ,88 ,93 ,27 }; int index = 0 ; for (index = 0 ; index < 10 ; index++) { if (SrcArray[index] != 0 && DstArray[index] != 0 ) { int sum = SrcArray[index] + DstArray[index]; if (sum % 2 == 0 ) printf ("偶数: %d \n" , sum); else printf ("基数: %d \n" , sum); } } system("pause" ); return 0 ; }
上述代码片段的逻辑并不复杂,仅仅只是循环内部嵌套双层判断,笔者思考片刻后即写出了与之对应的汇编代码;
.386p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc includelib kernel32.lib include msvcrt.inc includelib msvcrt.lib .data SrcArray DWORD 56,78,33,45,78,90,32,15,56,67 DstArray DWORD 59,77,89,23,11,45,67,88,93,27 index DWORD 0 sum DWORD 0 szFmt1 BYTE '基数: %d ',0dh,0ah,0 szFmt2 BYTE '偶数: %d ',0dh,0ah,0 .code main PROC mov dword ptr ds:[index],0 ; index = 0 jmp L1 L2: mov eax,dword ptr ds:[index] add eax,1 ; index++ mov dword ptr ds:[index],eax L1: cmp dword ptr ds:[index],10 ; index < 10 jge lop_end mov eax,dword ptr ds:[index]; cmp dword ptr ds:[SrcArray + eax * 4],0 je L2 ; SrcArray[index] != 0 mov eax,dword ptr ds:[index] cmp dword ptr ds:[DstArray + eax * 4],0 ; DstArray[index] != 0 je L2 ; ------------------------------------------ ; 另类加法,通过一个SrcArray定位DstArray完成加法 mov eax,dword ptr ds:[index] ; 获取因子 lea esi,dword ptr ds:[SrcArray] ; 取数组首地址 mov ebx,dword ptr ds:[esi + eax * 4] ; 获取 SrcArray[index] mov ecx,dword ptr ds:[esi + eax * 4 + 40] ; 获取 DstArray[index] add ebx,ecx ; SrcArray[index] + DstArray[index] mov dword ptr ds:[sum],ebx ; sum = SrcArray[index] + DstArray[index] mov eax,dword ptr ds:[sum] and eax,080000001h ; sum % 2 == 0 test eax,eax jne L3 invoke crt_printf,addr szFmt2,dword ptr ds:[sum] ; 偶数输出 jmp L2 L3: invoke crt_printf,addr szFmt1,dword ptr ds:[sum] ; 基数输出 jmp L2 lop_end: int 3 main ENDP END main
11.23 For嵌套乘法口诀表 该C++代码实现了乘法口诀表的打印。在两个for循环中,分别对x和y进行遍历,对每一次的遍历输出一个乘法口诀表的元素。代码中使用printf函数实现输出,并使用\n
进行换行。程序遍历打印了从11到99的所有乘积的结果,这就是乘法口诀表。
#include <stdio.h> #include <Windows.h> int main (int argc, char *argv[]) { for (int x = 1 ; x < 10 ; x++) { for (int y = 1 ; y <= x; y++) { int result = x*y; printf ("%d*%d=%-3d" , y, x, result); } printf ("\n" ); } system("pause" ); return 0 ; }
乘法口诀表的实现方法只需要嵌套两层FOR循环语句,在使用汇编语言实现之前我们可以先来构建出这个双层循环体,如下代码所示;
.386p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc includelib kernel32.lib include msvcrt.inc includelib msvcrt.lib .data x DWORD ? y DWORD ? szFmt BYTE '内层循环: %d 外层循环: %d ',0dh,0ah,0 szPr BYTE '----->',0dh,0ah,0 .code main PROC mov dword ptr ds:[x],1 ; int x = 1 jmp L1 L2: mov eax,dword ptr ds:[x] add eax,1 ; x++ mov dword ptr ds:[x],eax L1: cmp dword ptr ds:[x],10 ; x < 10 jge lop_end mov dword ptr ds:[y],1 ; y = 1 jmp L3 L5: mov eax,dword ptr ds:[y] add eax,1 ; y++ mov dword ptr ds:[y],eax L3: mov eax,dword ptr ds:[y] cmp eax,dword ptr ds:[x] ; y <= x jg L4 ; 执行的是循环体内部 mov eax,dword ptr ds:[x] mov ebx,dword ptr ds:[y] invoke crt_printf,addr szFmt,eax,ebx jmp L5 L4: ; 执行外层循环 invoke crt_printf,addr szPr jmp L2 lop_end: int 3 main ENDP END main
当有了双层循环体结构之后,我们只需要再其循环之上增加一个乘法计算功能即可,完整的计算流程如下所示;
.386p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc includelib kernel32.lib include msvcrt.inc includelib msvcrt.lib .data x DWORD ? y DWORD ? szFmt BYTE '%d * %d = %d ',0 szPr BYTE ' ',0dh,0ah,0 .code main PROC mov dword ptr ds:[x],1 ; int x = 1 jmp L1 L2: mov eax,dword ptr ds:[x] add eax,1 ; x++ mov dword ptr ds:[x],eax L1: cmp dword ptr ds:[x],10 ; x < 10 jge lop_end mov dword ptr ds:[y],1 ; y = 1 jmp L3 L5: mov eax,dword ptr ds:[y] add eax,1 ; y++ mov dword ptr ds:[y],eax L3: mov eax,dword ptr ds:[y] cmp eax,dword ptr ds:[x] ; y <= x jg L4 ; 执行的是循环体内部 mov eax,dword ptr ds:[x] imul eax,dword ptr ds:[y] invoke crt_printf,addr szFmt,dword ptr ds:[y],dword ptr ds:[x],eax jmp L5 L4: ; 执行外层循环 invoke crt_printf,addr szPr jmp L2 lop_end: int 3 main ENDP END main
11.24 For语句冒泡排序 该C++代码实现了冒泡排序算法对整型数组进行排序。在冒泡排序算法中,数组中每两个相邻的元素,如果前一个元素大于后一个元素,则交换这两个元素的位置。循环遍历数组多次,每次将未排序的最大值向数组末尾冒泡,直到数组中的所有元素都排好序。代码中使用两层for循环实现排序,内层循环从数组末尾开始,逐步向前遍历,交换相邻的两个元素。外层循环控制排序的遍历次数,只有在当前相邻两个数未排序时才进行交换。程序最终输出排序后的数组。
#include <stdio.h> #include <Windows.h> int main (int argc, char *argv[]) { int Array[10 ] = { 34 ,78 ,65 ,77 ,89 ,43 ,23 ,55 ,67 ,8 }; int x, y, temporary, ArraySize=10 ; for (x = 0 ; x < ArraySize - 1 ; x++) { for (y = ArraySize - 1 ; y > x; y--) { if (Array[y - 1 ] > Array[y]) { temporary = Array[y - 1 ]; Array[y - 1 ] = Array[y]; Array[y] = temporary; } } } for (int x = 0 ; x < 10 ; x++) { printf ("%d \n" , Array[x]); system("pause" ); return 0 ; }
由于冒泡排序牵扯到了数据交换所以汇编版本可能稍显负责,不过大体框架还是没有脱离二层循环,仅仅只是在二层循环内部增加了一个判断流程而已,其实如果认真构建相信读者也可以很容易的写出来。
.386p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc includelib kernel32.lib include msvcrt.inc includelib msvcrt.lib .data Array DWORD 34,78,65,77,89,43,23,55,67,8 x DWORD ? y DWORD ? Temporary DWORD ? ArraySize DWORD ? szFmt BYTE '%d --> %d ',0dh,0ah,0 .code main PROC ; 初始化的部分 mov dword ptr ds:[x],0 ; x=0 mov dword ptr ds:[ArraySize],10 ; ArraySize=10 ; 外层循环体 jmp L1 L2: mov eax,dword ptr ds:[x] add eax,1 ; x++ mov dword ptr ds:[x],eax L1: mov eax,dword ptr ds:[ArraySize] sub eax,1 ; ArraySize - 1 cmp dword ptr ds:[x],eax ; x < ArraySize jge lop_end ; 内层循环体内容 mov eax,dword ptr ds:[ArraySize] sub eax,1 mov dword ptr ds:[y],eax jmp L3 L4: mov eax,dword ptr ds:[y] sub eax,1 ; y-- mov dword ptr ds:[y],eax L3: mov eax,dword ptr ds:[y] cmp eax,dword ptr ds:[x] ; Array[y - 1] > Array[y] jle L2 ; 寻址y和y-1的位置 mov esi,dword ptr ds:[y] mov ebx,dword ptr ds:[Array + esi * 4] ; Array[y] mov edx,dword ptr ds:[Array + esi * 4 - 4] ; Array[y - 1] cmp edx,ebx jle L4 ; 数据交换 mov dword ptr ds:[Array + esi * 4],edx ; Array[y] = Array[y - 1] mov dword ptr ds:[Array + esi * 4 - 4],ebx ; Array[y - 1] = Array[y] ; invoke crt_printf,addr szFmt,ebx,edx jmp L4 jmp L2 lop_end: nop ; 执行打印函数 mov dword ptr ds:[Temporary],0 jmp L5 L7: mov eax,dword ptr ds:[Temporary] add eax,1 mov dword ptr ds:[Temporary],eax L5: mov eax,dword ptr ds:[Temporary] cmp eax,10 jge L6 lea esi,dword ptr ds:[Array] ; 取数组基地址 mov esi,dword ptr ds:[Array + eax * 4] ; 比例因子寻址 invoke crt_printf,addr szFmt,esi,esi jmp L7 L6: int 3 main ENDP END main
至此,汇编中的循环结构仿写就告一段落了,笔者提醒大家,由于汇编难度较大,且代码都是线性的,所以在编写之前要分析好主次关系,当有了主次关系之后,我们就需要静下心来,一个个构建,由外到内步步为营,其实汇编也并不是那么可怕。