Bootstrap

汇编语言学习(5)

更好的阅读体验 YinKai 's Blog

常量

​ NASM 提供了几个定义常量的指令,在上面我们使用过的有 EQU 指令,下面我们来重点介绍三个指令:

  • EQU
  • %assign
  • %define
EQU 指令

​ EQU 指令常用于定义常量,其语法如下:

CONSTANT_NAME EQU expression

​ 例如:

MY_NAME equ 'YinKai1'

​ 然后我们可以在代码中使用这个常量值,例如:

mov ecx, MY_NAME

​ EQU 语句的操作数也可以是表达式,如下:

length equ 20
width equ 10
AREA equ length * width

​ 下面示例演示了 EQU 指令的使用:

SYS_EXIT  equ 1
SYS_WRITE equ 4
STDIN     equ 0
STDOUT    equ 1

section .data
        msg1 db 'Hello, programmers!', 0xA, 0xD
        len1 equ $ - msg1

        msg2 db 'Welcome to the world of,', 0xA, 0xD
        len2 equ $ - msg2

        msg3 db 'Linux assembly programming!', 0xA, 0xD
        len3 equ $ - msg3

section .text
        global _start

_start:
        mov eax, SYS_WRITE
        mov ebx, STDOUT
        mov ecx, msg1
        mov edx, len1
        int 0x80

        mov eax, SYS_WRITE
        mov ebx, STDOUT
        mov ecx, msg2
        mov edx, len2
        int 0x80

        mov eax, SYS_WRITE
        mov ebx, STDOUT
        mov ecx, msg3
        mov edx, len3
        int 0x80

        mov eax, SYS_EXIT
        int 0x80

​ 上述程序会输出:

Hello, programmers!
Welcome to the world of,
Linux assembly programming!
%assign 指令

​ %assign 指令是在预处理阶段中用于定义数字常量的汇编指令。它类似于 EQU 指令,与 EQU 指令不同的是,%assign 允许在后续代码中重新定义常量的值,这对于在程序的不同部分或不同文件中使用相同的符号名但不同的值很有用。

​ 下面是一个使用样例:

%assign TOTAL 10

​ 然后在代码的后面,还可以再次定义:

%assign TOTAL 20
%define 指令

​ %define 指令是在汇编语言预处理阶段用于定义宏的指令,类似于 C 语言中的 #define 预处理指令,允许定义数字、字符串常量以及宏。

​ 使用示例如下,它会将 PTR 替换成 [EBP+4]

%define PTR [EBP+4]

section .text
	mov eax, PTR

:::warning

​ 上述三种指令都是区分大小写的。

:::

算术指令

INC 指令

​ INC 指令用于将操作数加一,它适用于可以位于寄存器或内存中的单个操作数。

​ 语法如下:

INC destination

​ destination 是要增加的操作数,可以是 8 位、16 位 或 32 位的寄存器或内存地址。

​ 使用示例:

INC EBX		; 增加 32 位寄存器 EBX 中的值
INC DL		; 增加 8 位寄存器 DL 中的值
INC [count]	; 增加存储在变量 count 的内存位置中的值
DEC 指令

​ DEC 指令用于将操作数减一,它适用于可以位于寄存器或内存中的单个操作数。

​ 语法如下:

DEC destination

​ destination 是要减小的操作数,可以是 8 位、16 位 或 32 位的寄存器或内存地址。

​ 使用示例:

segment .data
   count dw  0        ; 16位的变量 count,初始化为0
   value db  15       ; 8位的变量 value,初始化为15
	
segment .text
   inc word [count]   ; 将变量 count 的值增加1
   dec byte [value]   ; 将变量 value 的值减少1
	
   mov ebx, count     ; 将变量 count 的地址存入寄存器 ebx
   inc word [ebx]     ; 通过寄存器 ebx 增加变量 count 的值
	
   mov esi, value     ; 将变量 value 的地址存入寄存器 esi
   dec byte [esi]     ; 通过寄存器 esi 减少变量 value 的值
ADD 和 SUB 指令

​ ADD 和 SUB 指令用于执行字节、字和双字大小的二进制数据的加法和减法,它们分别用于8位、16位或32位操作数的加法和减法。

​ 语法如下:

ADD/SUB destination, source

destination 是目标操作数,source 是源操作数。这两个操作数可以是寄存器、内存地址或常数。

​ 使用示例:

section .data
    value1 dd 10       ; 双字(32位)大小的变量,初始化为10
    value2 dw 5        ; 字(16位)大小的变量,初始化为5
    result db 0        ; 字节(8位)大小的变量,用于存储结果

section .text
    ; 32位加法
    mov eax, [value1]   ; 将value1的值加载到寄存器eax
    add eax, 20         ; 将20加到eax中
    mov [value1], eax   ; 将结果存回value1

    ; 16位减法
    mov bx, [value2]    ; 将value2的值加载到寄存器bx
    sub bx, 3           ; 从bx中减去3
    mov [value2], bx    ; 将结果存回value2

    ; 8位加法
    mov al, 30          ; 将30加载到寄存器al
    add al, 15          ; 将15加到al中
    mov [result], al    ; 将结果存入result

​ 下面再给一个复杂一点的例子:

; 定义系统调用号
SYS_EXIT  equ 1
SYS_READ  equ 3
SYS_WRITE equ 4
STDIN     equ 0
STDOUT    equ 1

section .data 
   ; 提示用户输入第一个数字的消息
   msg1 db "Enter a digit ", 0xA,0xD 
   len1 equ $- msg1 

   ; 提示用户输入第二个数字的消息
   msg2 db "Please enter a second digit", 0xA,0xD 
   len2 equ $- msg2 

   ; 提示计算结果的消息
   msg3 db "The sum is: "
   len3 equ $- msg3

section .bss
   ; 存储用户输入的第一个数字
   num1 resb 2 

   ; 存储用户输入的第二个数字
   num2 resb 2 

   ; 存储计算结果
   res resb 1    

section	.text
   global _start    ; 声明为gcc使用的程序入口
	
_start:
   ; 输出提示信息,要求用户输入第一个数字
   mov eax, SYS_WRITE         
   mov ebx, STDOUT         
   mov ecx, msg1         
   mov edx, len1 
   int 0x80                

   ; 从标准输入读取用户输入的第一个数字,存储在num1中
   mov eax, SYS_READ 
   mov ebx, STDIN  
   mov ecx, num1 
   mov edx, 2
   int 0x80            

   ; 输出提示信息,要求用户输入第二个数字
   mov eax, SYS_WRITE        
   mov ebx, STDOUT         
   mov ecx, msg2          
   mov edx, len2         
   int 0x80

   ; 从标准输入读取用户输入的第二个数字,存储在num2中
   mov eax, SYS_READ  
   mov ebx, STDIN  
   mov ecx, num2 
   mov edx, 2
   int 0x80        

   ; 输出提示信息,指示即将显示计算结果
   mov eax, SYS_WRITE         
   mov ebx, STDOUT         
   mov ecx, msg3          
   mov edx, len3         
   int 0x80

   ; 将第一个数字转换为数字并存储在eax寄存器中
   mov eax, [num1]
   sub eax, '0'

   ; 将第二个数字转换为数字并存储在ebx寄存器中
   mov ebx, [num2]
   sub ebx, '0'

   ; 将eax和ebx相加,得到和,存储在res中
   add eax, ebx

   ; 将和转换为ASCII并存储在res中
   add eax, '0'
   mov [res], al

   ; 使用SYS_WRITE将计算得到的和输出到标准输出
   mov eax, SYS_WRITE        
   mov ebx, STDOUT
   mov ecx, res         
   mov edx, 1        
   int 0x80

exit:    
   ; 使用SYS_EXIT系统调用退出程序
   mov eax, SYS_EXIT   
   xor ebx, ebx 
   int 0x80

​ 上述代码向用户询问两个数字,分别将数字存储在 EAX 和 EBX 寄存器中,将值相加,将结果存储在内存位置 res 中,最后显示结果。

​ 编译运行后,输出如下:

Enter a digit
4
Please enter a second digit
5
9

​ 再来一个带有硬编码的例子:

section	.text
   global _start    ;must be declared for using gcc
	
_start:             ;tell linker entry point
   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	;file descriptor (stdout)
   mov	eax,4	;system call number (sys_write)
   int	0x80	;call kernel
	
   mov	ecx,sum
   mov	edx, 1
   mov	ebx,1	;file descriptor (stdout)
   mov	eax,4	;system call number (sys_write)
   int	0x80	;call kernel
	
   mov	eax,1	;system call number (sys_exit)
   int	0x80	;call kernel
	
section .data
   msg db "The sum is:", 0xA,0xD 
   len equ $ - msg   
   segment .bss
   sum resb 1

​ 上述代码编译运行后会输出:

The sum is:
7
MUL/IMUL 指令

​ 有两条指令用于将二进制数据相乘,MUL 指令处理无符号数据,IMUL 指令处理有符号数据。

​ 语法如下:

MUL/IMUL multiplier

​ 两种情况下的被乘数都位于累加器中,具体取决于被乘数和乘数的大小,并且生成的乘积也取决于操作数的大小,存储在两个寄存器中。

​ 下面我们来看看三种不同情况下的 MUL 指令:

序号场景
1当两个字节相乘时
被乘数在 AL 寄存器中,乘数是内存中或另一个寄存器中的一个字节。结果存放在 AX 寄存器中,乘积的高 8 位存放在 AH 中,乘积的低 8 位存放在 AL 中
2当两个单字值相乘时
被乘数应该在 AX 寄存器中,乘数是内存或另一个寄存器中的一个字。
生成的结果是双字,需要两个寄存器。高位(最左边)部分存储在 DX 中,低位(最右边)部分存储在 AX 中
3当两个双字相乘时
当两个双字值相乘时,被乘数应位于 EAX 中,乘数是存储在内存或另一个寄存器中的双字值。
生成的乘积存储在 EDX:EAX 寄存器中,即高位 32 位存储在 EDX 寄存器中,低位 32 位存储在 EAX 寄存器中。

​ 示例:将 3 与 2 相乘,并显示结果:

section .text
   global _start    ; 必须声明为gcc使用的程序入口
	
_start:
   ; 将字符 '3' 转换为数字并存储在 AL 中
   mov al, '3'
   sub al, '0'
   
   ; 将字符 '2' 转换为数字并存储在 BL 中
   mov bl, '2'
   sub bl, '0'
   
   ; 使用 MUL 指令将 AL 和 BL 相乘,结果存储在 AX 中
   mul bl
   
   ; 将结果转换为字符并存储在 AL 中
   add al, '0'
   
   ; 将结果存储在内存位置 res 中
   mov [res], al
   
   ; 准备输出消息
   mov ecx, msg
   mov edx, len
   mov ebx, 1       ; 文件描述符 (stdout)
   mov eax, 4       ; 系统调用号 (sys_write)
   int 0x80         ; 调用内核输出消息到标准输出
   
   ; 准备输出计算结果
   mov ecx, res
   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 result is:", 0xA,0xD    ; 输出消息
    len equ $- msg
   
section .bss
    res resb 1       ; 存储计算结果的空间

​ 编译输出结果如下:

The result is:
6
DIV/IDIV 指令

​ 除法运算也有两个指令,DIV(除法)指令用于无符号数据,IDIV(整数除法)指令用于有符号数据。

:::warning

​ 除法运算生成两个元素 - 一个和一个余数。 在乘法的情况下,不会发生溢出,因为使用双倍长度寄存器来保存乘积。 然而,在除法的情况下,可能会发生溢出。 如果发生溢出,处理器会产生中断。

:::

​ 使用的语法如下:

DIV/IDIV	divisor

​ 下面根据不同操作数大小分为三种不同的情况:

序号场景
1当除数为1 byte时 −假定被除数位于 AX 寄存器(16 位)中。 除法后,商存入 AL 寄存器,余数存入 AH 寄存器。
2当除数为 1 个 word 时 −假定被除数为 32 位长,位于 DX:AX 寄存器中。 高 16 位在 DX 中,低 16 位在AX中。 除法后,16 位商进入 AX 寄存器,16 位余数进入 DX 寄存器。
3除数为 doubleword 时 −假设被除数为 64 位长并位于 EDX:EAX 寄存器中。 高阶 32 位在 EDX 中,低阶 32 位在 EAX 中。 除法后,32 位商进入 EAX 寄存器,32 位余数进入 EDX 寄存器。

​ 使用示例:

section .text
   global _start    ; 必须声明为gcc使用的程序入口

_start:
   ; 将字符 '8' 转换为数字并存储在 AX 中
   mov ax, '8'
   sub ax, '0'
   
   ; 将字符 '2' 转换为数字并存储在 BL 中
   mov bl, '2'
   sub bl, '0'
   
   ; 使用 DIV 指令将 AX 除以 BL,商存储在 AL 中,余数存储在 AH 中,默认操作累加器(通常为 AX)
   div bl
   
   ; 将商(AL)转换为字符并存储在 AX 中
   add ax, '0'
   
   ; 将结果存储在内存位置 res 中	
   mov [res], ax
   
   ; 准备输出消息
   mov ecx, msg
   mov edx, len
   mov ebx, 1       ; 文件描述符 (stdout)
   mov eax, 4       ; 系统调用号 (sys_write)
   int 0x80         ; 调用内核输出消息到标准输出
   
   ; 准备输出计算结果
   mov ecx, res
   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 result is:", 0xA,0xD    ; 输出消息
    len equ $- msg

    section .bss
    res resb 1       ; 存储计算结果的空间

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

The result is:
4

逻辑指令

​ 处理器指令集提供了 AND、OR、XOR、TEST、NOT 这几个布尔逻辑指令,以满足程序的需求,其中包括对位进行测试、设置和清除。

​ 这些指令的使用格式如下:

序号说明格式
1ANDAND operand1, operand2
2OROR operand1, operand2
3XORXOR operand1, operand2
4TESTTEST operand1, operand2
5NOTNOT operand1

​ 在这些指令中,第一个操作数可以是内存或寄存器中的内容,第二个操作数可以是内存或寄存器或立即数(常量)。

:::warning

​ 直接在内存之间的操作是不被允许的!!!

:::

​ 这些指令比较或匹配操作数的位,并根据结果设置 CF(进位标志)、OF(溢出标志)、PF(奇偶校验标志)、SF(符号标志)和 ZF(零标志),这样使得程序能够进行灵活的逻辑运算,同时提供了对操作结果的状态标志进行监测的能力。

AND 指令

​ AND 指令是用于执行按位 AND(与)运算的指令。按位与运算返回一个结果并将结果存储到第一个操作数中,其中仅当两个操作数的相应位都为 1 时,结果位才为 1;反之则结果位为 0.

使用示例:假设 BL 寄存器包含 0011 1010,我们想要把高位清零,可以使用 AND 指令与 OFH(0000 1111)进行运算,就可以达到高位清零的效果:

AND BL, OFH

​ 如果我们想要检验一个数是奇数还是偶数,可以通过对 AL 寄存器中的数字与 01H(0000 0001) 进行 AND 运算,可以有效检查低位:

AND AL, 01H ; AND 操作,检查最低有效位
JZ  EVEN_NUMBER ; 如果结果为零,跳转到 EVEN_NUMBER(偶数)

JZ:条件跳转指令,含义是 ‘Jump if Zero’,即如果结果为零(ZF 标志位为 1),则执行跳转。

​ 下面再来看一个完整的示例:演示了如何使用 AND 指令来检查和显示奇偶性

section .text
global _start

_start:
   ; 将数字 8 放入寄存器 ax
   mov   ax, 8h
   ; 对寄存器 ax 与 1 进行 AND 操作,检查最低位
   and   ax, 1
   ; 如果结果为零,跳转到 evnn(偶数)
   jz    evnn

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

evnn:   
   ; 显示偶数消息
   mov   ah,  09h
   mov   eax, 4         ; 系统调用号(sys_write)
   mov   ebx, 1         ; 文件描述符(stdout)
   mov   ecx, even_msg  ; 要写入的消息
   mov   edx, len1      ; 消息长度
   int   0x80           ; 调用内核

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

section .data
even_msg  db  'Even Number!' ; 显示偶数消息
len1  equ  $ - even_msg 
   
odd_msg db  'Odd Number!'    ; 显示奇数消息
len2  equ  $ - odd_msg
OR 指令

​ OR 指令用于执行按位或运算,支持逻辑表达式。按位或运算的规则是,如果任一操作数的相应位为1,或者两个操作数的相应位都为1,则结果位为1。如果两个位都为0,则结果位为0。同样,运算的结果是存储在第一个操作数中,操作数可以是寄存器或内存中的值。

​ 下面来看一个完整的使用样例:我们将 3 与 5 进行 OR 操作,并输出结果

section .bss
    res resb 1  ; 用于存储 OR 运算的结果

section .text
    global _start

_start:
    ; 将值5存储在寄存器AL中
    mov al, 5
    ; 将值3存储在寄存器BL中
    mov bl, 3
    ; 对AL和BL寄存器执行按位或运算,结果应为7
    or al, bl
    ; 将结果转换为ASCII码
    add al, byte '0'

    ; 将结果存储在res变量中
    mov [res], al
    ; 调用sys_write系统调用,将结果写入stdout
    mov eax, 4
    mov ebx, 1
    mov ecx, res
    mov edx, 1
    int 0x80

outprog:
    ; 调用sys_exit系统调用,退出程序
    mov eax, 1
    int 0x80

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

7
XOR 指令

​ XOR(异或)指令用于执行按位异或运算。按位异或的规则是,当且仅当操作数中的位不同时,结果位被设置为1。如果操作数的位相同(均为0或均为1),则结果位被清零为0。

​ 对于指令 XOR EAX, EAX,它将寄存器 EAX 中的值与自身进行异或操作,这实际上会将寄存器清零。因为任何值与自身进行异或运算的结果都是0。

TEST 指令

​ TEST 指令的工作方式类似于 AND 指令,但与 AND 指令不同的是,它不会更改第一个操作数的内容。该指令执行按位与运算,并根据结果设置条件标志(CF、OF、PF、SF 和 ZF)。这使得 TEST 指令非常适合用于检查某个值的特定位状态,而不更改该值。

​ 使用示例:

TEST AL, 01H
JZ EVEN_NUMBER

​ 上述代码演示了如何使用 TEST 指令来检查 AL 寄存器中的数字是否为偶数。如果 AL 寄存器中的值与 01H 进行按位与运算后结果为零(ZF 标志被设置),则跳转到标签 EVEN_NUMBER。

NOT 指令

​ NOT 指令执行按位 NOT 运算,即反转操作数中的每一位。操作数可以位于寄存器或存储器中。

​ 示例:

Operand1:    0101 0011
After NOT -> Operand1:    1010 1100

​ 上述示例展示了如何使用 NOT 指令对二进制数字进行按位取反操作。

条件执行

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

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

  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/JZJump Equal or Jump Zero当零标志位(ZF)被设置时跳转
JNE/JNZJump not Equal or Jump Not Zero当零标志位(ZF)未被设置时跳转
JG/JNLEJump Greater or Jump Not Less/Equal当溢出标志(OF)、符号标志(SF)和零标志(ZF)符合条件时跳转
JGE/JNLJump Greater/Equal or Jump Not Less当溢出标志(OF)和符号标志(SF)符合条件时跳转
JL/JNGEJump Less or Jump Not Greater/Equal当溢出标志 (OF) 和符号标志 (SF) 符合条件时跳转。
JLE/JNGJump Less/Equal or Jump Not Greater当溢出标志 (OF) 、符号标志 (SF) 和零标志 (ZF) 符合条件时跳转。

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

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

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

说明描述已测试标志
JXCZJump if CX is Zero当 CX 寄存器的值为零时跳转。
JCJump If Carry当进位标志 (CF) 被设置时跳转。
JNCJump If No Carry当进位标志 (CF) 未被设置时跳转。
JOJump If Overflow当溢出标志 (OF) 被设置时跳转。
JNOJump If No Overflow当溢出标志 (OF) 未被设置时跳转。
JP/JPEJump Parity or Jump Parity Even当奇偶标志 (PF) 被设置时跳转。
JNP/JPOJump No Parity or Jump Parity Odd当奇偶标志 (PF) 未被设置时跳转。
JSJump Sign (negative value)当符号标志 (SF) 被设置时跳转。
JNSJump 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

;