汇编语言学习(6)

更好的阅读体验 YinKai 's BLog。

条件执行

​ 在汇编语言中,实现条件执行的机制主要通过多个循环和分支指令完成,这些指令能够改变程序的控制流程。

​ 条件执行一般分为两种情况:

  1. 无条件跳转:

    无条件跳转是通过 JMP 指令实现的,在这种情况下,条件执行涉及将程序的控制转移到不是紧随当前正在执行指令的指令的地址上。这种跳转转移可以是向前的,以执行一组新的指令,也可以是向后的,以程序执行相同的步骤。

  2. 条件跳转

    条件跳转是通过一组跳转指令 j 来实现的,其中条件是根据特定条件而定。这些条件指令通过中断正常的指令执行流程来转移控制,通过修改指令指针寄存器(IP)中的偏移值来实现。

CMP 指令

​ 在讨论条件指令前,我们先来看看 CMP 指令。

​ CMP 指令是一种用于比较两个操作数的指令,通常在条件执行中使用。该指令主要通过对一个操作数与另一个操作数进行减法运算来实现比较,以确定这两个操作数是否相等。

​ 值得注意的是,CMP 指令执行比较操作,但不会影响目标或源操作数的值。

​ 语法如下:

CMP destination, source

​ 其中目标操作数可以是位于寄存器或内存中,而源操作数可以是常量(立即数)数据、寄存器或内存。

​ 使用示例:

CMP DX, 00
JE	L7
...
L7: ...

​ 比较 DX 寄存器的值与零,如果相等,则跳转到标签 L7.

​ CMP 指令通常用于比较计数器值是否达到执行循环所需的次数,以下是一个经典应用的示例:

INC EDX
CMP EDX, 10
JLE LP1

​ 比较计数器 EDX 是否达到 10,如果未达到 10 则跳转到 LP1 标签。

无条件跳转

​ 无条件跳转是通过 JMP 指令实现的,该指令使程序控制流立即转移到指定标签的地址。

​ JMP 指令的语法如下:

JMP label

​ 使用示例:

MOV AX, 00	; 将 AX 初始化为 0
MOV BX, 00	; 将 BX 初始化为 0
MOV CX, 01	; 将 CX 初始化为 1
L20:
ADD AX, 01	; 将 AX 递增
ADD BX, AX	; 将 AX 加到 BX 中
SHL CX, 1	; 将 CX 左移一位,从而使 CX 值倍增
JMP L20		; 重复执行上述语句

​ 上面程序中的 SHL 指令用于将二进制数向左移指定的位数,以达到倍增的效果。在这个例子中,JMP 指令用于无条件跳转到标签 L20,从而创建一个无限循环,反复执行 MOV、ADD 和 SHL 指令。

条件跳转

​ 条件跳转是指在程序执行过程中,根据特定条件的满足与否,控制流会转移到指定的目标指令。

​ 条件跳转指令的选择取决于不同的条件和数据状态。

​ (1)以下是用于有符号数据上的算术运算的条件跳转指令,以及它们所检查的标志:

说明 描述 已测试标志
JE/JZ Jump Equal or Jump Zero 当零标志位(ZF)被设置时跳转
JNE/JNZ Jump not Equal or Jump Not Zero 当零标志位(ZF)未被设置时跳转
JG/JNLE Jump Greater or Jump Not Less/Equal 当溢出标志(OF)、符号标志(SF)和零标志(ZF)符合条件时跳转
JGE/JNL Jump Greater/Equal or Jump Not Less 当溢出标志(OF)和符号标志(SF)符合条件时跳转
JL/JNGE Jump Less or Jump Not Greater/Equal 当溢出标志 (OF) 和符号标志 (SF) 符合条件时跳转。
JLE/JNG Jump Less/Equal or Jump Not Greater 当溢出标志 (OF) 、符号标志 (SF) 和零标志 (ZF) 符合条件时跳转。

(2)以下是用于无符号数据的逻辑运算的条件跳转指令,以及它们所检查的标志:

说明 描述 已测试标志
JE/JZ Jump Equal or Jump Zero 当零标志位 (ZF) 被设置时跳转。
JNE/JNZ Jump not Equal or Jump Not Zero 当零标志位 (ZF) 未被设置时跳转。
JA/JNBE Jump Above or Jump Not Below/Equal 当进位标志 (CF) 和零标志 (ZF) 符合条件时跳转。
JAE/JNB Jump Above/Equal or Jump Not Below 当进位标志 (CF) 符合条件时跳转。
JB/JNAE Jump Below or Jump Not Above/Equal 当进位标志 (CF) 符合条件时跳转。
JBE/JNA Jump Below/Equal or Jump Not Above 当辅助进位标志 (AF) 和进位标志 (CF) 符合条件时跳转。

​ (3)另外,以下条件跳转指令具有特殊用途并检查相应标志的值:

说明 描述 已测试标志
JXCZ Jump if CX is Zero 当 CX 寄存器的值为零时跳转。
JC Jump If Carry 当进位标志 (CF) 被设置时跳转。
JNC Jump If No Carry 当进位标志 (CF) 未被设置时跳转。
JO Jump If Overflow 当溢出标志 (OF) 被设置时跳转。
JNO Jump If No Overflow 当溢出标志 (OF) 未被设置时跳转。
JP/JPE Jump Parity or Jump Parity Even 当奇偶标志 (PF) 被设置时跳转。
JNP/JPO Jump No Parity or Jump Parity Odd 当奇偶标志 (PF) 未被设置时跳转。
JS Jump Sign (negative value) 当符号标志 (SF) 被设置时跳转。
JNS Jump No Sign (positive value) 当符号标志 (SF) 未被设置时跳转。

​ 先来看一个简单的示例:

CMP AL, BL
JE EQUAL
CMP AL, BH
JE EQUAL
CMP AL, CL
JE EQUAL
NON_EQUAL:...
EQUAL:...

​ 上述代码在执行过程中会根据 AL 寄存器和 BL、BH、CL 寄存器的比较结果,若相等则跳转到标签 EQUAL,否则执行标签 NON_EQUAL 后续的指令。

示例

​ 下面的程序通过比较三个两位数变量,找到其中的最大值,并将其结果输出到标准输出:

section	.text
   global _start         ; 必须声明为使用gcc

_start:	                 ; 告诉链接器入口点
   mov   ecx, [num1]     ; 将num1的值加载到ecx寄存器
   cmp   ecx, [num2]     ; 将ecx与num2的值比较
   jg    check_third_num ; 如果ecx大于num2,跳转到check_third_num标签
   mov   ecx, [num2]     ; 如果跳转到check_third_num,将num2的值加载到ecx寄存器
   
check_third_num:

   cmp   ecx, [num3]     ; 将ecx与num3的值比较
   jg    _exit            ; 如果ecx大于num3,跳转到_exit标签
   mov   ecx, [num3]     ; 如果跳转到_exit,将num3的值加载到ecx寄存器
   
_exit:
   
   mov   [largest], ecx   ; 将最大值存储在largest变量中
   mov   ecx, msg         ; 将消息字符串的地址加载到ecx寄存器
   mov   edx, len         ; 将消息字符串的长度加载到edx寄存器
   mov   ebx,1            ; 文件描述符(标准输出)
   mov   eax,4            ; 系统调用号(sys_write)
   int   0x80             ; 调用内核

   mov   ecx, largest     ; 将存储最大值的变量地址加载到ecx寄存器
   mov   edx, 2           ; 将输出的字节数加载到edx寄存器
   mov   ebx,1            ; 文件描述符(标准输出)
   mov   eax,4            ; 系统调用号(sys_write)
   int   0x80             ; 调用内核
    
   mov   eax, 1           ; 将系统调用号设置为退出程序
   int   80h              ; 调用内核

section	.data
   
   msg db "The largest digit is: ", 0xA,0xD ; 定义包含消息字符串的数据段
   len equ $- msg        ; 计算消息字符串的长度
   num1 dd '47'          ; 定义包含两位数值的数据段
   num2 dd '22'          ; 定义包含两位数值的数据段
   num3 dd '31'          ; 定义包含两位数值的数据段

segment .bss
   largest resb 2        ; 定义一个字节的空间,用于存储最大值

​ 上面提供了对每个指令和标签的解释,帮助理解代码的功能和执行流程。

​ 程序编译运行后会输出结果如下:

he largest digit is:
47

循环

​ 在汇编语言中循环可以用 JMP 指令实现,如下代码演示了如何使用 JMP 指令执行循环体 10 次:

MOV CL, 10
L1:

DEC CL
JNZ L1

​ 处理器指令集还有专门用于实现循环的循环指令——LOOP,其使用语法如下:

LOOP label

​ 这里的 label 是目标标签,用于标识跳转指令中的目标位置,LOOP 指令假定 ECX 寄存器中包含循环计数,执行 LOOP 指令时,ECX 寄存器中的值递减,控制跳转到目标标签,知道 ECX 寄存器中的值达到零为止。

​ 因此上面的 JMP 实现的循环,可以改写为:

MOV ECX, 10
l1:

loop l1

​ 下面看一个比较复杂的样例,我们利用 loop 指令输出 1~9 的数字:

section	.text
   global _start        ;必须声明以供gcc使用
	
_start:	                ;告诉链接器入口点
   mov ecx, 10          ;将计数器初始化为10,表示要输出的数字个数
   mov eax, '1'         ;将ASCII码为'1'的字符赋值给寄存器eax
	
l1:
   mov [num], eax       ;将eax中的字符存储到变量num所指的内存位置
   mov eax, 4           ;将系统调用号4(sys_write)存储到寄存器eax
   mov ebx, 1           ;将文件描述符1(标准输出)存储到寄存器ebx
   push ecx             ;将计数器值保存到栈中,以备后续循环使用
	
   mov ecx, num         ;将变量num的地址存储到寄存器ecx
   mov edx, 1           ;表示要写入的字节数,此处为1
   int 0x80             ;触发系统调用,将字符输出到标准输出
	
   mov eax, [num]       ;将变量num所指的内存位置的值加载到eax
   sub eax, '0'         ;将字符转换为数字
   inc eax              ;增加数字
   add eax, '0'         ;将数字转换回字符
   pop ecx              ;从栈中恢复计数器值
   loop l1              ;循环,直到计数器为零
	
   mov eax, 1           ;系统调用号1(sys_exit)
   int 0x80             ;调用内核退出程序
section	.bss
num resb 1              ;定义一个字节的未初始化数据,用于存储字符

​ 上述代码编译运行后,输出的结果如下:

123456789

数字

​ 数值数据通常以二进制表示,算术指令对二进制数据进行运算,当数字显示在屏幕上或从键盘输入时,它们都是 ASCII 形式。

​ 到目前为止,我们已经使用过这些 ASCII 形式的输入数据转换为二进制进行算术计算,并将结果转换回二进制。

​ 示例如下:

section	.text
   global _start        ;必须声明以便使用gcc

_start:                ;告诉链接器入口点
   mov    eax, '3'
   sub    eax, '0'

   mov    ebx, '4'
   sub    ebx, '0'
   add    eax, ebx
   add    eax, '0'

   mov    [sum], eax
   mov    ecx, msg
   mov    edx, len
   mov    ebx, 1         ;文件描述符(stdout)
   mov    eax, 4         ;系统调用号(sys_write)
   int    0x80          ;调用内核

   mov    ecx, sum
   mov    edx, 1
   mov    ebx, 1         ;文件描述符(stdout)
   mov    eax, 4         ;系统调用号(sys_write)
   int    0x80          ;调用内核

   mov    eax, 1         ;系统调用号(sys_exit)
   int    0x80          ;调用内核

section .data
msg db "The sum is:", 0xA,0xD 
len equ $ - msg
segment .bss
sum resb 1

​ 上面的代码编运行后会输出的结果如下:

The sum is:
7

​ 虽然此类转换可以达到一样的效果,但这种转换会产生开销,并且汇编语言编程允许以更有效的方式处理二进制形式的数字。

​ 十进制有两种形式表示:

  • ASCII 形式
  • BCD 或 二进制编码的十进制形式
ASCII 表示

​ 在 ASCII 表示中,二进制数字存储为 ASCII 字符串,例如十进制数 1234 存储为

31 	32	33	34H

​ 其中 31H 为 1 的 ASCII 值,32H 为 2 的 ASCII 值,以此推类。

​ 有四个指令用于处理 ASCII 表示的数字:

  • AAA:添加后 ASCII 调整
  • AAS:减法后调整 ASCII
  • AAM:乘法后的 ASCII 调整
  • ADD:除法前的 ASCII 调整

​ 这些指令不接受任何操作数,并假定所需的操作数位于 AL 寄存器中。

​ 下面给出一个使用示例:这段程序的主要目的是执行BCD减法,将 ASCII 字符 ‘9’ 减去 ‘3’,并将结果输出到终端

section .text
   global _start        ; 必须声明为使用 gcc

_start:                 ; 告诉链接器入口点
   sub     ah, ah       ; 清零 AH 寄存器
   mov     al, '9'      ; 将 '9' 存入 AL 寄存器
   sub     al, '3'      ; 用 '3' 减去 AL 寄存器中的值
   aas                   ; 进行 ASCII 调整,将 AH:AL 转换为两位的十进制数值
   or      al, 30h      ; 将 AL 寄存器的值与 0x30 进行逻辑或操作,转换为 ASCII 码
   mov     [res], ax    ; 将调整后的结果存储到 res 变量中

   mov  edx, len        ; 消息长度
   mov  ecx, msg        ; 要写入的消息
   mov  ebx, 1          ; 文件描述符 (stdout)
   mov  eax, 4          ; 系统调用号 (sys_write)
   int  0x80            ; 调用内核

   mov  edx, 1          ; 消息长度
   mov  ecx, res        ; 要写入的消息
   mov  ebx, 1          ; 文件描述符 (stdout)
   mov  eax, 4          ; 系统调用号 (sys_write)
   int  0x80            ; 调用内核

   mov  eax, 1          ; 系统调用号 (sys_exit)
   int  0x80            ; 调用内核

section .data
msg db 'The Result is:',0xa  ; 消息字符串,包括换行符
len equ $ - msg              ; 计算消息长度
section .bss
res resb 1                   ; 用于存储结果的变量
BCD

​ BCD 是一种用于二进制表示十进制数字的方法。有两种表示形式

解压缩的 BCD 表示

​ 在解压缩的 BCD 表示形式中,每个字节存储十进制数字的二进制等价物。例如数字 1234 以解压缩的 BCD 表示存储为:

01 02 03 04H

​ 每个字节的低四位和高四位分别表示一个十进制数字。这种表示法中,每个字节都直接对应于一个十进制数位。对于解压缩的 BCD 表示,有两个相关的指令:

  • AAM(ASCII Adjust After Multiplication): 乘法后的 ASCII 调整指令,用于在乘法后调整结果,以确保结果的每个字节表示一个有效的十进制数字。
  • AAD(ASCII Adjust Before Division): 除法前的 ASCII 调整指令,用于在除法前调整输入,以确保输入的每个字节表示一个有效的十进制数字。
压缩 DCB 表示

​ 在压缩 BCD 表示形式中,每个数字使用四位二进制数存储。两个十进制数字被打包成一个字节。例如,数字 1234 以压缩 BCD 表示存储为:

12 34H

​ 每个字节的高四位和低四位分别表示一个十进制数字。压缩 BCD 表示形式不支持乘法和除法,但有两个相关的指令:

  • DAA(Decimal Adjust After Addition): 加法后小数调整指令,用于在加法后调整结果,以确保结果的每个字节表示一个有效的十进制数字。
  • DAS(Decimal Adjust After Subtraction): 减法后小数调整指令,用于在减法后调整结果,以确保结果的每个字节表示一个有效的十进制数字。

​ BCD 表示法提供了一种有效地在计算机中存储和处理十进制数字的方法。解压缩的 BCD 适用于支持乘法和除法的场景,而压缩 BCD 使用于加法和减法的场景。

​ 下面程序将两个 5位十进制数相加,并显示总和:

section .text
   global _start        ; 必须声明为使用 gcc

_start:	                ; 告诉链接器入口点

   mov     esi, 4       ; 指向右边的数字(从低位开始)
   mov     ecx, 5       ; 数字的位数
   clc                  ; 清除进位标志 CF

add_loop:  
   mov     al, [num1 + esi]  ; 从 num1 中加载一个数字的 ASCII 字符到 AL 寄存器
   adc     al, [num2 + esi]  ; 将 num2 中对应位置的 ASCII 字符加到 AL 寄存器,考虑进位 CF
   aaa                        ; ASCII 调整,将 AL 寄存器的值调整为两位的十进制数值
   pushf                      ; 将标志寄存器的值(包括 CF)推送到栈上
   or      al, 30h            ; 将 AL 寄存器的值转换为 ASCII 字符
   popf                       ; 恢复标志寄存器的值
   mov     [sum + esi], al    ; 将结果存储到 sum 中
   dec     esi                ; 移动到下一位
   loop    add_loop           ; 循环直到所有位都处理完

   mov     edx, len           ; 消息长度
   mov     ecx, msg           ; 要写入的消息
   mov     ebx, 1             ; 文件描述符 (stdout)
   mov     eax, 4             ; 系统调用号 (sys_write)
   int     0x80               ; 调用内核输出消息

   mov     edx, 5             ; 消息长度
   mov     ecx, sum           ; 要写入的消息
   mov     ebx, 1             ; 文件描述符 (stdout)
   mov     eax, 4             ; 系统调用号 (sys_write)
   int     0x80               ; 调用内核输出结果

   mov     eax, 1             ; 系统调用号 (sys_exit)
   int     0x80               ; 调用内核退出程序

section .data
msg db 'The Sum is:', 0xa     ; 消息字符串,包括换行符
len equ $ - msg               ; 计算消息长度
num1 db '12345'               ; 第一个数字
num2 db '23456'               ; 第二个数字
sum  db '     '               ; 用于存储结果的变量

​ 编译运行后的输出结果如下:

The Sum is:
35801

字符串

​ 在汇编语言中处理字符串时,我们可以采用两种方式来指定字符串的长度:

  1. 显示存储字符串长度:

    我们可以使用 $ 位置计算器符号来显示存储字符串长度,该符号表示位置计数器的当前值。如下例:

    msg db 'Hello, world!', 0xa
    len equ $ - msg
    

    或者我们也可以显示设置字符串的长度,如:

    msg db 'Hello, world!', 0xa
    len equ 13
    
  2. 使用哨兵字符:

    这种方式是存储带有尾随哨兵字符的字符串,而不是显示存储字符串的长度。哨兵是一个特殊字符,不会出现在字符串中,用于分隔字符串的结束。如下:

    message db 'I am loving it!', 0
    

    字符串的长度由尾随的零字符标志着,这种方法的优势在于不需要显示存储字符串的长度,而是依赖于遇到的第一个特殊字符串来确定字符串的结束。

字符串指令

​ 字符串指令用于处理字符串数据,其中包括移动、加载、存储和比较等操作。这些指令涉及源操作数和目标操作数。

​ 在 32 位段中,通常使用 ESI 和 EDI 寄存器分别指向源和目标;而在 16 位段中,相应地使用 SI 和 DI 寄存器。

​ 处理字符串有五个基本的指令,它们是:

  • **MOVS:**该指令将 1 个字节、字或双字的数据从内存位置移动到另一个位置
  • **LODS:**该指令从内存中加载,如果操作数是一个字节,则加载到 AL 寄存器;如果操作数是一个字,则加载到 AX 寄存器;如果操作数是双字,则加载到 EAX 寄存器。
  • **STOS:**该指令将数据从寄存器 (AL、AX 或 EAX)存储到内存。
  • **CMPS:**该指令比较内存中的两个数据项。数据可以是字节大小、字或双字。
  • **SCAS:**该指令将寄存器(AL、AX 或 EAX)的内容与内存中项目的内容进行比较。

​ 这些指令可以通过使用重复前缀来进行重复操作,例如REP MOVS将连续地执行MOVS指令,直到ECX寄存器的值为零。

​ 在这些指令中,ES:DI(或EDI)和 DS:SI(或ESI)寄存器分别指向目标操作数和源操作数,其中 SI 通常与 DS(数据段)相关联,而DI则与ES(额外段)相关联。

​ 对于 16 位地址,使用 SI 和 DI 寄存器;而对于 32 位地址,使用 ESI 和 EDI 寄存器。

MOVS 指令

​ MOVS 指令用于将数据项(字节、字或双字)从源字符串复制到目标字符串。 DS:SI 指向源字符串,ES:DI 指向目标字符串。

​ 如下例:

section .text
   global _start        ; 必须为了使用 gcc 声明
	
_start:                ; 告诉链接器入口点
   mov ecx, len        ; 将字符串 s1 的长度加载到 ECX 寄存器
   mov esi, s1         ; 将源字符串 s1 的起始地址加载到 ESI 寄存器
   mov edi, s2         ; 将目标字符串 s2 的起始地址加载到 EDI 寄存器
   cld                 ; 清除方向标志 DF,确保 rep 指令中的字符串操作向高地址方向进行
   
   rep movsb           ; 使用 rep 前缀,将字符串从 s1 复制到 s2

   mov edx, 20         ; 指定要写入的字节数
   mov ecx, s2         ; 指定要写入的字符串的地址
   mov ebx, 1          ; 文件描述符(stdout)
   mov eax, 4          ; 系统调用号(sys_write)
   int 0x80            ; 调用内核

   mov eax, 1          ; 系统调用号(sys_exit)
   int 0x80            ; 调用内核

section .data
s1 db 'Hello, world!', 0  ; 字符串 1,以零结尾
len equ $-s1              ; 计算字符串 s1 的长度

section .bss
s2 resb 20                ; 目标字符串 s2,分配 20 个字节的空间

​ 编译运行后输出结果如下:

Hello, world!
LODS 指令

​ 我们通过模拟凯撒密码加密的方式,来熟悉一下 LDOS 指令的用法:将数据中的每个字母替换为两个字母的位移来加密数据,即 a 将被 c 替换,b 与 d 等。

section .text
   global _start         ; 必须为了使用 gcc 声明
	
_start:                  ; 告诉链接器入口点
   mov    ecx, len      ; 将字符串 s1 的长度加载到 ECX 寄存器
   mov    esi, s1       ; 将源字符串 s1 的起始地址加载到 ESI 寄存器
   mov    edi, s2       ; 将目标字符串 s2 的起始地址加载到 EDI 寄存器

loop_here:
   lodsb                 ; 加载 AL 寄存器中的字节到 AL,同时将 SI 递增
   add al, 2             ; 将 AL 中的值增加 2
   stosb                 ; 存储 AL 寄存器的值到目标地址中的字节,同时将 DI 递增
   loop loop_here        ; 通过 ECX 寄存器的计数来重复上述过程,直到计数为零
   cld                   ; 清除方向标志 DF,确保 rep 指令中的字符串操作向高地址方向进行
   rep movsb             ; 使用 rep 前缀,将剩余的字符串从 s1 复制到 s2

   mov     edx, 20       ; 指定要写入的字节数
   mov     ecx, s2       ; 指定要写入的字符串的地址
   mov     ebx, 1        ; 文件描述符(stdout)
   mov     eax, 4        ; 系统调用号(sys_write)
   int     0x80          ; 调用内核

   mov     eax, 1        ; 系统调用号(sys_exit)
   int     0x80          ; 调用内核

section .data
s1 db 'password', 0      ; 源字符串,以零结尾
len equ $-s1             ; 计算字符串 s1 的长度

section .bss
s2 resb 10               ; 目标字符串 s2,分配 10 个字节的空间
STOS 指令

​ STOS 指令将数据项从 AL(对于字节 - STOSB)、AX(对于字 - STOSW)或 EAX(对于双字 - STOSD)复制到内存中 ES:DI 指向的目标字符串。

​ 下面示例演示如何使用 LODS 和 STOS 指令将大写字符串转换为其小写值。

section .text
   global _start        ; 必须为了使用 gcc 声明
	
_start:                ; 告诉链接器入口点
   mov ecx, len        ; 将字符串 s1 的长度加载到 ECX 寄存器
   mov esi, s1         ; 将源字符串 s1 的起始地址加载到 ESI 寄存器
   mov edi, s2         ; 将目标字符串 s2 的起始地址加载到 EDI 寄存器

loop_here:
   lodsb                 ; 加载 AL 寄存器中的字节到 AL,同时将 SI 递增
   or al, 20h            ; 使用按位或运算将大写字母转换为小写字母
   stosb                 ; 存储 AL 寄存器的值到目标地址中的字节,同时将 DI 递增
   loop loop_here        ; 通过 ECX 寄存器的计数来重复上述过程,直到计数为零
   cld                   ; 清除方向标志 DF,确保 rep 指令中的字符串操作向高地址方向进行
   rep movsb             ; 使用 rep 前缀,将剩余的字符串从 s1 复制到 s2

   mov edx, 20           ; 指定要写入的字节数
   mov ecx, s2           ; 指定要写入的字符串的地址
   mov ebx, 1            ; 文件描述符(stdout)
   mov eax, 4            ; 系统调用号(sys_write)
   int 0x80              ; 调用内核

   mov eax, 1            ; 系统调用号(sys_exit)
   int 0x80              ; 调用内核

section .data
s1 db 'HELLO, WORLD', 0 ; 源字符串,以零结尾
len equ $-s1             ; 计算字符串 s1 的长度

section .bss
s2 resb 20               ; 目标字符串 s2,分配 20 个字节的空间
CMPS 指令

​ CMPS 指令比较两个字符串。 该指令比较 DS:SI 和 ES:DI 寄存器指向的一个字节、一个字或一个双字的两个数据项,并相应地设置标志。 您还可以将条件跳转指令与此指令一起使用。

​ 下面代码演示了如何使用 CMPS 指令比较两个字符串是否相等:

section .text
   global _start            ; 必须为了使用 gcc 声明
	
_start:                     ; 告诉链接器入口点
   mov esi, s1              ; 将源字符串 s1 的起始地址加载到 ESI 寄存器
   mov edi, s2              ; 将目标字符串 s2 的起始地址加载到 EDI 寄存器
   mov ecx, lens2           ; 将字符串 s2 的长度加载到 ECX 寄存器
   cld                      ; 清除方向标志 DF,确保比较字符串的方向是从高地址到低地址

   repe cmpsb               ; 重复比较 ESI 和 EDI 指向的字节,直到不相等或者 ECX 变为零
   jecxz equal              ; 如果 ECX 为零,说明字符串相等,跳转到 equal 标签

   ; 如果不相等,则执行以下代码
   mov eax, 4               ; 系统调用号 (sys_write)
   mov ebx, 1               ; 文件描述符 (stdout)
   mov ecx, msg_neq         ; 要写入的字符串 "Strings are not equal!"
   mov edx, len_neq         ; 字符串长度
   int 80h                   ; 调用内核

   jmp exit                  ; 跳转到 exit 标签

equal:
   mov eax, 4               ; 系统调用号 (sys_write)
   mov ebx, 1               ; 文件描述符 (stdout)
   mov ecx, msg_eq          ; 要写入的字符串 "Strings are equal!"
   mov edx, len_eq          ; 字符串长度
   int 80h                   ; 调用内核

exit:
   mov eax, 1               ; 系统调用号 (sys_exit)
   mov ebx, 0               ; 退出码
   int 80h                   ; 调用内核

section .data
s1 db 'Hello, world!', 0    ; 第一个字符串
lens1 equ $-s1              ; 计算字符串 s1 的长度

s2 db 'Hello, there!', 0    ; 第二个字符串
lens2 equ $-s2              ; 计算字符串 s2 的长度

msg_eq db 'Strings are equal!', 0xa ; 相等时输出的字符串
len_eq equ $-msg_eq          ; 相等时字符串的长度

msg_neq db 'Strings are not equal!' ; 不相等时输出的字符串
len_neq equ $-msg_neq         ; 不相等时字符串的长度
SCAS 指令

​ SCAS 指令用于搜索字符串中的特定字符或字符集。 要搜索的数据项应位于 AL(对于 SCASB)、AX(对于 SCASW)或 EAX(对于 SCASD)寄存器中。 要搜索的字符串应该在内存中并由 ES:DI(或 EDI)寄存器指向。

​ 下面的代码演示了如何在字符串中查找是否存在某个字符:

section .text
   global _start        ; 必须为了使用 gcc 声明
	
_start:                ; 告诉链接器入口点

   mov ecx, len         ; 将字符串 my_string 的长度加载到 ECX 寄存器
   mov edi, my_string   ; 将目标字符串 my_string 的起始地址加载到 EDI 寄存器
   mov al, 'e'          ; 设置要查找的字符为 'e'
   cld                  ; 清除方向标志 DF,确保 scasb 指令向前比较

   repne scasb          ; 从 EDI 指向的内存位置开始,逐个比较每个字节与 AL 寄存器的值,直到找到相等的字节或者 ECX 为零
   je found             ; 如果找到相等的字节,跳转到 found 标签

   ; 如果没有找到,则执行以下代码
   mov eax, 4            ; 系统调用号 (sys_write)
   mov ebx, 1            ; 文件描述符 (stdout)
   mov ecx, msg_notfound  ; 要写入的字符串 "not found!"
   mov edx, len_notfound  ; 字符串长度
   int 80h               ; 调用内核

   jmp exit              ; 跳转到 exit 标签

found:
   mov eax, 4            ; 系统调用号 (sys_write)
   mov ebx, 1            ; 文件描述符 (stdout)
   mov ecx, msg_found     ; 要写入的字符串 "found!"
   mov edx, len_found     ; 字符串长度
   int 80h               ; 调用内核

exit:
   mov eax, 1            ; 系统调用号 (sys_exit)
   mov ebx, 0            ; 退出码
   int 80h               ; 调用内核

section .data
my_string db 'hello world', 0  ; 字符串
len equ $-my_string            ; 计算字符串 my_string 的长度

msg_found db 'found!', 0xa      ; 找到时输出的字符串
len_found equ $-msg_found       ; 找到时字符串的长度

msg_notfound db 'not found!'    ; 未找到时输出的字符串
len_notfound equ $-msg_notfound ; 未找到时字符串的长度

下表提供了各种版本的字符串指令和假定的操作数空间。

基本指令 操作数位于 Byte 操作 Word 操作 Double word 操作
MOVS ES:DI, DS:SI MOVSB MOVSW MOVSD
LODS AX, DS:SI LODSB LODSW LODSD
STOS ES:DI, AX STOSB STOSW STOSD
CMPS DS:SI, ES: DI CMPSB CMPSW CMPSD
SCAS ES:DI, AX SCASB SCASW SCASD
重复前缀

​ 当在字符串指令之前设置 REP 前缀时,例如 - REP MOVSB,会导致基于 CX 寄存器中的计数器重复指令。 REP 执行该指令,将 CX 减 1,并检查 CX 是否为零。 它重复指令处理,直到 CX 为零。

​ 方向标志(DF)决定操作的方向:

  • CLD(清楚方向标志,DF = 0)进行从左到右的操作
  • STD(设置方向标志,DF = 1)使操作从右到左

​ REP 前缀还有一些变体,如下:

  • REP:无条件重复,它重复该操作,直到 CX 为零。
  • REPE 或 REPZ:有条件重复,当零标志指示等于/零时,它会重复该操作。当 ZF 指示不等于/零或 CX 为零时,它会停止。
  • REPNE 或 REPNZ:也是有条件重复。 当零标志指示不等于/零时,它会重复该操作。 当 ZF 指示等于/零或 CX 递减至零时,它会停止

数组

​ 汇编器的数据定义指令用于为变量分配存储空间,该变量也可以用一些特定值进行初始化,初始化值可以以十六进制、十进制或二进制形式指定。

​ 例如我们可以使用下面任一方式定义单词变量 “months”

months dw 12
months dw ocH
months dw o110B

​ 数据定义指令也可以用于定义一维数组,下面是一个一维数组的定义:

numbers dw 34, 45, 56, 67, 78, 89

​ 上面定义了六个字组成的数组,每个字都用数字 34, 45, 56, 67, 78, 89 进行了初始化。分配了 2x6 = 12 字节的连续内存空间。第一个数字的符号地址为 numbers,第二个为 numbers + 2,以此推类。

​ 对同一值进行多次初始化还可以用 TIMES 指令:

numbers times 8 dw 0 = numbers dw 0, 0, 0, 0, 0, 0, 0, 0
示例

​ 下面的程序将数组中的 3 个字节的值求和输出:

section .text
global _start   ; 必须为了使用链接器 (ld)
	
_start:
 		
   mov  eax, 3    ; 要求将3个字节进行求和 
   mov  ebx, 0    ; EBX 将存储总和
   mov  ecx, x    ; ECX 将指向要进行求和的当前元素

top:  
   add  ebx, [ecx] ; 将当前元素的值加到总和上

   add  ecx, 1     ; 移动指针到下一个元素
   dec  eax        ; 计数器递减
   jnz  top        ; 如果计数器不为0,继续循环

done: 

   add  ebx, '0'   ; 将结果转换为ASCII字符
   mov  [sum], ebx ; 完成,将结果存储在 "sum" 中

display:

   mov  edx, 1     ; 消息长度
   mov  ecx, sum   ; 要写入的消息
   mov  ebx, 1     ; 文件描述符 (stdout)
   mov  eax, 4     ; 系统调用号 (sys_write)
   int  0x80       ; 调用内核
	
   mov  eax, 1     ; 系统调用号 (sys_exit)
   int  0x80       ; 调用内核

section .data
global x
x:    
   db  2
   db  4
   db  3

sum: 
   db  0           ; 用于存储总和的地方,初始化为0

​ 编译运行后的结果为:

9

你可能感兴趣的:(汇编,学习,汇编)