Important Instructions and Syntax
此内容是以MASM编写的,你将使用Visual C/C++内联汇编来编程,因此数据元素的声明有所不同,但概念和指令集(instruction sets)相同。
一、General-Purpose Registers
寄存器是CPU内的命名存储单元,设计上优化了速度。
Registers are named storage locations inside the CPU, optimized for speed.
解释:一般用途寄存器是CPU内部用于快速存取数据的特殊存储位置,主要用于处理和操作数据。
1. Accessing Parts of Registers(访问寄存器的部分)
这张图展示了如何访问32位寄存器的不同部分。一个32位寄存器(如EAX)可以拆分为16位的AX和8位的AH(高8位)与AL(低8位)。这意味着可以通过使用不同的位宽名称(8位、16位或32位)来访问寄存器的不同部分,这种方式适用于EAX、EBX、ECX和EDX寄存器。
(这种分块设计可以在不更改整个寄存器内容的情况下,只修改特定部分,提高了操作的灵活性)
2. Index and Base Registers(索引和基址寄存器)
索引和基址寄存器(如ESI、EDI、EBP和ESP)。这些寄存器既有32位的名称(如ESI)也有16位的名称(如SI),但它们不具备像通用寄存器(EAX等)那样的8位部分。这些寄存器常用于存储地址、指向数据或管理堆栈操作。
3. Some Specialized Register Uses
EAX通常用作累加器,ECX用作循环计数器,ESP是堆栈指针,EBP是扩展帧指针,ESI和EDI则用于索引操作。段寄存器(如CS、DS、SS等)则用于划分不同的内存段,分别对应代码段、数据段和堆栈段等。
EIP(指令指针)和EFLAGS寄存器的功能。EIP用于存储下一条指令的位置,控制程序的执行流。EFLAGS则包含多个标志位,用于反映程序运行时的状态(如零标志、进位标志等),每个标志都是一个二进制位,用来控制或指示特定的状态。
二、Instructions
汇编器将指令编译成机器码,运行时由CPU执行。
Assembled into machine code by the assembler and executed by the CPU at runtime.
此处使用的是Intel IA-32指令集。指令由四个部分组成:
• Label(标签):用于标记代码位置--optional
• Mnemonic(助记符):指令名称,用于指定操作类型(如MOV、ADD)-- required
• Operand(操作数):指令的具体操作对象,取决于指令类型。
• Comment(注释):--optional
1. Labels(place markers)
标签用作位置标记,标记代码或数据的地址(偏移量offset)。标签需遵循标识符(identifer)规则,分为数据标签和代码标签:
• Data label:必须唯一,例如myArray(不带冒号colon)。
• Code label:通常作为跳转指令的目标,例如L1:(带冒号)。
target of jump and loop instructions
(标签在代码中用于标记特定位置,便于跳转和引用。数据标签标记变量位置,代码标签标记指令位置)
2. Mnemonics(memory aid) and Operands
助记符是指令的缩写,用于帮助记忆指令的功能,如MOV、ADD、SUB等。
操作数则是指令的操作对象,可以是常量、常量表达式、寄存器或内存地址(数据标签)。
Constants and constant expressions are often called immediate values
3. Comments
• single -line comments:以分号(semicolon)开头。
• Multi-line comments:COMMENT指令可以用于多行注释,格式是指定一个特定字符(a programmer-chosen character)作为开始和结束标记。如 ;、#
COMMENT *
……
*
4. Instruction Format Examples
不同操作数数量的指令格式示例:
(1) No operands(无操作数)
• stc:设置进位标志(Carry Flag)。这是一个无操作数的指令,它只影响CPU标志寄存器的状态,而不需要其他的操作数。
(2) One operand(单操作数)
• inc eax:对寄存器 eax 的值加 1。inc 是递增指令,用于将指定操作数的值增加 1。
• inc myByte:对内存位置 myByte 的值加 1。inc 可以作用于内存位置,将该内存位置的值增加 1。
(3) Two operands(双操作数)
• add ebx, ecx:将寄存器 ecx 的值加到寄存器 ebx 上,结果存储在 ebx 中。add 指令用于加法操作,通常用于两个寄存器之间的数据运算。
• sub myByte, 25:将常量 25 从内存位置 myByte 的值中减去,结果存储在 myByte 中。sub 指令用于减法操作,可以对内存数据进行减法。
• add eax, 36 * 25:将表达式 36 * 25 的结果加到寄存器 eax 上。add 指令可以接受常量表达式作为操作数,这里 36 * 25 作为一个立即数添加到 eax 中。
三、MOV Instruction
用于将数据从源位置(source)复制到目标位置(destination)(传输数据)
MOV destination, source.
• 只允许一个内存操作数(memory operand),CS、EIP、IP不能作为目标(destination),不能直接向段寄存器(如DS、CS、SS等)赋值立即数。
No immediate to segment moves.
• 立即数(Immediate Value):直接的数值,立即数只能作为源操作数,不能作为目标操作数。因为立即数是常量,不能被修改
• 寄存器(Register):用于存储临时数据。在x86架构中,寄存器的位宽类型必须和操作数位宽一致。
• 内存地址(Memory Address):指向一个内存位置的变量或常量,在代码中常通过变量名表示。内存到内存的数据传递不被允许,至少一个操作数必须是寄存器。
需要通过寄存器中转
mov al, bVal
mov bVal2, al
• 指令指针寄存器(如EIP):存储下一条将要执行指令的地址,它是只读的,不能直接通过mov指令修改。只能通过跳转指令(如JMP、CALL)间接改变
.data
count BYTE 100 ; 定义一个8位的变量count,值为100
wVal WORD 2 ; 定义一个16位的变量wVal,值为2
.code
mov bl, count ; 将8位变量count的值移动到8位寄存器bl中
mov ax, wVal ; 将16位变量wVal的值移动到16位寄存器ax中
mov count, al ; 将8位寄存器al的值移动到8位变量count中
.data段:是一个伪指令(directive),用于定义程序中的静态数据(变量和常量),数据段中的变量可以初始化也可以不初始化。
• BYTE:表示8位(1字节)数据,例如 BYTE 100 表示一个8位的变量,值为100。
• WORD:表示16位(2字节)数据,例如 WORD 2 表示一个16位的变量,值为2。
• DWORD:表示32位(4字节)数据,例如 DWORD 5 表示一个32位的变量,值为5。
• .code段:是一个伪指令(directive),用于定义程序的执行代码,即指令序列。程序在执行时会从代码段开始逐条指令执行。
错误代码部分:
mov al, wVal ;al是8位寄存器,而wVal是16位变量,位宽不匹配,因此报错。
mov ax, count ;ax是16位寄存器,而count是8位变量,位宽不匹配,因此报错。
.data
bVal BYTE 100 ; 定义一个8位的变量bVal,值为100
bVal2 BYTE ? ; 定义一个8位的变量bVal2,未初始化
wVal WORD 2 ; 定义一个16位的变量wVal,值为2
dVal DWORD 5 ; 定义一个32位的变量dVal,值为5
.code
mov ds, 45 ; 错误:不允许将立即数45直接移动到段寄存器ds
mov esi, wVal ; 错误:esi是32位寄存器,而wVal是16位变量,位宽不匹配
mov eip, dVal ; 错误:eip是指令指针寄存器,只读,不能作为目标
mov 25, bVal ; 错误:立即数25不能作为目标操作数
mov bVal2, bVal ; 错误:内存到内存的移动不被允许
Zero Extension and Sign Extension
• Zero Extension:movzx ax, bl 用零扩展将小值复制到较大目标寄存器,目标(destination)必须是寄存器。
mov bl, 10001111b ; 将二进制数10001111加载到8位寄存器BL中
movzx ax, bl ; 使用MOVZX将BL扩展到16位寄存器AX,结果为00000000 10001111
• Sign Extension:movsx ax, bl 用符号(sign bit)扩展将小值复制到较大目标寄存器。MOVSX指令的目标必须是寄存器,不能是内存位置。
mov bl, 10001111b ; 将二进制数10001111加载到8位寄存器BL中
movsx ax, bl ; 使用MOVSX将BL扩展到16位寄存器AX,结果为11111111 10001111
由于bl的最高位是1,表示一个负数,因此ax的高位也被填充为1,保持了符号的一致性。
二、XCHG Instruction
XCHG(Exchange)指令用于交换两个操作数的值。此指令有以下限制:
• 至少有一个操作数(operands)必须是寄存器。
• 不允许立即数(immediate operands)作为操作数。
• 不能直接交换两个内存位置,至少需要一个寄存器参与。
.data
var1 WORD 1000h ; 定义一个16位变量var1,值为1000h
var2 WORD 2000h ; 定义一个16位变量var2,值为2000h
.code
xchg ax, bx ; 交换两个16位寄存器AX和BX的值
xchg ah, al ; 交换两个8位寄存器AH和AL的值
xchg var1, bx ; 交换内存变量var1和寄存器BX的值
xchg eax, ebx ; 交换两个32位寄存器EAX和EBX的值
xchg var1, var2 ; 错误:两个操作数都是内存位置(two memory operands)
三、INC and DEC Instructions
用于将目标操作数(destination operand)加1或减(subtract)1,操作数可以是寄存器或内存。
•INC destination 等同于 destination = destination + 1
• DEC destination 等同于 destination = destination - 1
INC and DEC Examples
.data
myWord WORD 1000h ; 定义一个16位变量myWord,初始值为1000h
myDword DWORD 10000000h ; 定义一个32位变量myDword,初始值为10000000h
.code
inc myWord ; myWord的值增加1,变为1001h
dec myWord ; myWord的值减少1,恢复为1000h
inc myDword ; myDword的值增加1,变为10000001hmov ax, 0FFh ; 将0FFh加载到AX,AX = 00FFh
inc ax ; AX递增1,导致进位,结果为0100h
mov ax, 0FFh ; 重新将00FFh加载到AX
inc al ; AL递增1(FFh + 1),结果为00h,AX变为0000h
AX 包含 AH 和 AL,但 AH 和 AL 是独立的 8 位寄存器,分别表示 AX 的高 8 位和低 8 位
result:
• AX = 0000h
• AH = 00h
• AL = 00h
• inc al 指令只对 AX 的低8位 AL 进行递增操作。
• 当前 AL = FFh(即255)。inc al 将 FFh 加1,因为 AL 是8位寄存器,加1后会产生溢出,使 AL 回到 00h。
• 由于 AL 是 AX 的一部分,当 AL 变为 00h 时,整个 AX 的值也会随之改变。
.data
myByte BYTE 0FFh, 0 ; 定义一个8位变量myByte,第一个值为0FFh,第二个值为0
.code
mov al, myByte ; 将myByte的第一个字节0FFh加载到AL中,AL = 0FFh
mov ah, [myByte + 1] ; 将myByte的第二个字节0加载到AH中,AH = 0
dec ah ; AH递减1,结果为FFh(-1)
inc al ; AL递增1,溢出,结果为00h
dec ax ; AX递减1,结果为FFFFh
• [myByte + 1] 表示访问 myByte 数组的第二个字节。
在 16 位寄存器(如 AX)中,先对低 8 位(AL)减,然后对高 8 位(AH)减。当 AL 减到 0 后,再从 AH 借位
四、ADD and SUB Instructions
• ADD指令:将源值(source value)加到目标值
ADD destination, source。
• SUB指令:从目标值中减去源值
SUB destination, source。
Same operand rules as for the MOV instruction
ADD and SUB Examples
.data
var1 DWORD 10000h ; 定义32位变量var1,值为10000h
var2 DWORD 20000h ; 定义32位变量var2,值为20000h
.code
mov eax, var1 ; 将var1的值加载到EAX,EAX = 00010000h
add eax, var2 ; 将var2的值加到EAX,EAX = 00030000h
add ax, 0FFFFh ; 将0FFFFh加到AX,EAX = 0003FFFFh
add eax, 1 ; 将1加到EAX,导致进位,EAX = 00040000h
sub ax, 1 ; 将AX减去1,结果EAX = 0004FFFFh
五、NEG (Negate) Instruction
Reverses the sign of an operand. The operand can be a register or memory.
NEG Example
.data
valB BYTE -1 ; 定义一个8位变量valB,值为-1(FFh)
valW WORD +32767 ; 定义一个16位变量valW,值为+32767
.code
mov al, valB ; 将valB的值加载到AL,AL = -1 (FFh)
neg al ; 反转AL的符号,AL = +1 (01h)
neg valW ; 反转valW的符号,valW = -32767
负数通常使用 二进制补码(Two’s Complement) 来表示。
• 找到对应的正数值。
• 对所有位取反,然后 加 1,得到补码。
假设我们用 8 位来表示 -1:
1. 首先,写出 +1 的二进制表示:0000 0001。
2. 将每一位取反:得到 1111 1110。
3. 加 1:1111 1110 + 1 = 1111 1111。
因此,1111 1111(即 FFh)在 8 位补码系统中代表 -1。
Suppose AX contains –32,768 and we apply NEG to it. Will the result be valid?
解释:在 16 位补码系统中,能表示的最小负数是 -32768,对应的二进制是 1000 0000 0000 0000(即 8000h)。但是在 16 位补码系统中,32768 超出了范围(因为最大正数是 +32767),所以无法表示这个结果,导致溢出。溢出标志 OF 会被置为 1
这个数值是唯一一个在 16 位系统中无法取反的数, 最小负数 -32768 在 16 位系统中已经是特殊处理的结果,直接由 1000 0000 0000 0000 表示,无需额外的加一操作。
NEG Instruction and the Flags
• 处理器(processor)使用 SUB 0, operand 来实现 NEG 操作。任何非零操作数都会设置进位标志(CF)。
Any nonzero operand causes the Carry flag to be setazz
• 每个 NEG 操作的进位标志(CF)和溢出标志(OF)的状态如下:
.data
valB BYTE 1, 0 ; 定义8位变量valB,值为1和0
valC SBYTE -128 ; 定义有符号字节变量valC,值为-128
.code
neg valB ; 反转valB(1),CF = 1,OF = 0
neg [valB + 1] ; 反转valB(0),CF = 0,OF = 0
neg valC ; 反转valC(-128),CF = 1,OF = 1
• NEG 指令会设置进位标志(CF),如果操作数为非零,CF 通常为 1,即使结果由于溢出而无法正确表示。(从零中减去一个非零数,就必须借位)
• 对于无法表示的取负结果(如 -128 取负),会导致溢出标志(OF)置为 1,结果不变。
Implementing Arithmetric Expressions
Rval = -Xval + (Yval - Zval)
.data
Rval DWORD ? ; 定义一个双字变量 Rval,初始值未定
Xval DWORD 26 ; 定义一个双字变量 Xval,值为 26
Yval DWORD 30 ; 定义一个双字变量 Yval,值为 30
Zval DWORD 40 ; 定义一个双字变量 Zval,值为 40.code
mov eax, Xval ; 将 Xval 的值加载到 EAX 中
neg eax ; 对 EAX 中的值取负,EAX = -26mov ebx, Yval ; 将 Yval 的值加载到 EBX 中
sub ebx, Zval ; 计算 EBX - Zval,结果存入 EBX,EBX = -10add eax, ebx ; 将 EBX 的值加到 EAX 中,EAX = -26 + (-10) = -36
mov Rval, eax ; 将 EAX 中的结果存入 Rval,Rval = -36
在汇编中,至少一个操作数必须是寄存器或内存位置。无法直接对两个立即数进行加减运算,需要通过寄存器或内存位置作为中介。
六、Flags Affected by Arithmetic
• 算术逻辑单元(ALU)有许多状态标志(status flags),用来反映算术和位运算的结果
outcome of arithmetic (and bitwise) operations.
• 这些标志取决于目标操作数(destination operand)的内容
Essential Flags:
• 零标志(Zero flag):当结果(destination)为零时设置。
• 符号标志(Sign flag):当结果为负时设置。
• 进位标志(Carry flag):当无符号值超出范围时设置。(set when the unsigned value is out of range)
• 溢出标志(Overflow flag):当有符号值超出范围时设置。( set when the signed value is out of range)
• MOV指令不会影响任何标志位(flags)
Concept Map
• 条件跳转指令使用状态标志,提供分支逻辑
Conditional jumps use status flags to provide branching logic.
以下是对图片内容的详细注释:
Zero Flag (ZF)
零标志(ZF)用于指示操作的结果是否为零。当运算结果为零时,ZF 被设置为1。
mov cx, 1 ; 将1加载到寄存器 CX
sub cx, 1 ; CX = CX - 1,此时 CX = 0,ZF = 1(运算结果为零)
mov ax, 0FFFFh ; 将 0FFFFh (65535) 加载到 AX
inc ax ; AX = AX + 1,结果 AX = 0,ZF = 1(溢出到0)
inc ax ; AX = AX + 1,结果 AX = 1,ZF = 0(结果不为零)
• 提示:当标志等于1时,它被“设置”;当等于0时,它被“清除”。
Sign Flag (SF)
符号标志(SF)用于指示操作数的符号。当运算结果为负数时,SF 被设置为1;当结果为正数时,SF 被清除。
mov cx, 0 ; 将 0 加载到寄存器 CX
sub cx, 1 ; CX = CX - 1,结果 CX = -1,SF = 1(结果为负)
add cx, 2 ; CX = CX + 2,结果 CX = 1,SF = 0(结果为正)
解释:符号标志(SF)复制了目标操作数的最高位,以指示符号。
mov al, 0 ; 将 0 加载到 AL
sub al, 1 ; AL = AL - 1,结果 AL = 1111 1111b,SF = 1(负数)
add al, 2 ; AL = AL + 2,结果 AL = 0000 0001b,SF = 0(正数)
七、OFFSET Operator 偏移操作符
OFFSET 操作符返回标签到其所在段起始位置的字节距离。分为保护模式和实模式:
• protected mode: 32 bits (保护模式下返回 32 位偏移量)
• real mode: 16 bits (实模式下返回 16 位偏移量)
标签 是一种用于标识程序中的特定位置的名称。
• 变量的地址
• 代码中的跳转位置
• 数据和代码的区分
标签可以存在于段内,例如数据标签位于数据段中,代码标签位于代码段中
.data ; 数据段
myVar BYTE 10 ; 定义一个字节变量 myVar,值为 10,myVar 是标签
.code ; 代码段
start: ; start 是一个代码标签,用于标识代码段的开始
mov al, myVar ; 将 myVar 的值加载到 AL 寄存器中
jmp start ; 跳转到 start 位置
解释:在保护模式下,我们通常使用“平面内存模型”(Flat Memory Model),即单一段模型(single segment)。
平面内存模型 表示将所有代码、数据和堆栈放入一个逻辑上连续的内存空间中(一个段)。这使得所有数据和代码可以通过一个简单的线性地址来访问,而不需要分多个段。
OFFSET Examples 偏移示例
assume that the data segment begins at 00404000h
.data
bVal BYTE ? ; 定义一个字节类型(1字节)变量 bVal
wVal WORD ? ; 定义一个字类型(2字节)变量 wVal
dVal DWORD ? ; 定义一个双字类型(4字节)变量 dVal
dVal2 DWORD ? ; 定义另一个双字类型变量 dVal2
.code
mov esi, OFFSET bVal ; 将 bVal 的偏移地址加载到 ESI 中,ESI = 00404000h
mov esi, OFFSET wVal ; 将 wVal 的偏移地址加载到 ESI 中,ESI = 00404001h
mov esi, OFFSET dVal ; 将 dVal 的偏移地址加载到 ESI 中,ESI = 00404003h
mov esi, OFFSET dVal2 ; 将 dVal2 的偏移地址加载到 ESI 中,ESI = 00404007h
Relating to C/C++
OFFSET 操作符 返回的值类似于 C/C++ 中的指针(pointer),它表示的是内存中的某个位置。在汇编和 C/C++ 中可以如下实现:
• C++ 代码:
char array[1000]; // 定义一个字符数组 array,大小为 1000 字节
char *p = array; // 定义一个字符指针 p,指向 array 的起始位置
• 汇编代码:
.data
array BYTE 1000 DUP(?) ; 定义一个大小为 1000 字节的字符数组 array
.code
mov esi, OFFSET array ; 将 array 的偏移地址加载到寄存器 ESI 中
DUP(Duplicate)操作符 用于定义数组或重复初始化多个相同的值。它允许你在声明数据时使用相同的值多次,从而简化了初始化数组或多个连续内存位置的过程
在汇编中,OFFSET array 就像 C++ 中的指针 p,都指向 array 的起始位置。
八、TYPE Operator
TYPE 操作符 返回数据声明中每个元素的大小(size),以字节为单位。
.data
var1 BYTE ? ; 定义一个字节变量 var1
var2 WORD ? ; 定义一个字变量 var2
var3 DWORD ? ; 定义一个双字变量 var3
var4 QWORD ? ; 定义一个四字变量 var4
.code
mov eax, TYPE var1 ; TYPE var1 = 1,因为 BYTE 的大小是 1 字节
mov eax, TYPE var2 ; TYPE var2 = 2,因为 WORD 的大小是 2 字节
mov eax, TYPE var3 ; TYPE var3 = 4,因为 DWORD 的大小是 4 字节
mov eax, TYPE var4 ; TYPE var4 = 8,因为 QWORD 的大小是 8 字节
九、LENGTHOF Operator
LENGTHOF 操作符 用于计算(count)单个数据声明中的元素数量。
.data
byte1 BYTE 10,20,30 ; byte1 包含 3 个字节元素
array1 WORD 30 DUP(?),0,0 ; array1 包含 32 个字元素
array2 WORD 5 DUP(3 DUP(?)) ; array2 包含 15 个字元素
array3 DWORD 1,2,3,4 ; array3 包含 4 个双字元素
digitStr BYTE "12345678",0 ; digitStr 包含 9 个字节字符(8 个字符 + 1 个空字符)
.code
mov ecx, LENGTHOF array1 ; LENGTHOF array1 = 32
十、SIZEOF Operator
SIZEOF 操作符 返回的数据大小等于 LENGTHOF 乘以 TYPE 的结果。(multiplying LENGTHOF by TYPE)
.data
byte1 BYTE 10,20,30 ; byte1 的 SIZEOF = 3(元素数量) * 1(字节大小) = 3
array1 WORD 30 DUP(?),0,0 ; array1 的 SIZEOF = 32 * 2 = 64
array2 WORD 5 DUP(3 DUP(?)) ; array2 的 SIZEOF = 15 * 2 = 30
array3 DWORD 1,2,3,4 ; array3 的 SIZEOF = 4 * 4 = 16
digitStr BYTE "12345678",0 ; digitStr 的 SIZEOF = 9 * 1 = 9
.code
mov ecx, SIZEOF array1 ; SIZEOF array1 = 64
十一、Spanning Multiple Lines
数据声明可以跨多行定义(spans nultiple lines),如果每行(除了最后一行)以逗号结尾。LENGTHOF 和 SIZEOF 操作符会包含所有属于该声明的行。
.data
array WORD 10,20, ; array 包含 6 个字元素
30,40,
50,60
.code
mov eax, LENGTHOF array ; LENGTHOF array = 6(元素个数)
mov ebx, SIZEOF array ; SIZEOF array = 12(6 * 2 字节)
如果数组的每一行是独立的 WORD 声明,LENGTHOF 和 SIZEOF 操作符仅应用于第一个声明。
array identifies only the first WORD declaration
.data
array WORD 10,20 ; 第一行定义了 array,包含 2 个字元素
WORD 30,40 ; 另一个独立的 WORD 声明
WORD 50,60 ; 再一个独立的 WORD 声明
.code
mov eax, LENGTHOF array ; LENGTHOF array = 2(只包含第一个声明)
mov ebx, SIZEOF array ; SIZEOF array = 4(2 * 2 字节)
在这种情况下,array 仅引用第一个 WORD 声明的两个元素,后续的 WORD 声明被视为独立的声明。
十二、Label Directive
• 作用:LABEL 指令为一个内存位置分配(assigns)一个新的标签,同时指定该标签的数据类型。
• 不分配额外存储:LABEL 只是给已有的存储位置添加一个别名,不会分配新的存储空间(allocate storage)。
• 去除 (remove)PTR 操作符的需要:使用 LABEL 可以避免在访问不同类型的数据时使用 PTR 操作符。
.data
dwList LABEL DWORD ; 为现有的存储位置分配一个 DWORD 类型的标签 dwList
wordList LABEL WORD ; 为同一存储位置分配一个 WORD 类型的标签 wordList
intList BYTE 00h,10h,00h,20h ; 定义一个字节数组 intList.code
mov eax, dwList ; 将 dwList(DWORD 类型)的值加载到 EAX 中,结果20001000h
mov cx, wordList ; 将 wordList(WORD 类型)的值加载到 CX 中,结果为 1000h
mov dl, intList ; 将 intList 的第一个字节加载到 DL 中,结果为 00h
• 小端序表示:将 20h 作为最高位,00h 为次高位,依此类推,得到 EAX = 20001000h。
十三、JMP 和 LOOP 指令
JMP instruction
• JMP指令是一个无条件(unconditional)跳转指令,直接将控制流跳转到同一过程(procedure)中的指定标签,而无需检查任何条件。
JMP target
• target 指定了控制流(control)要跳转到的标签或地址。
• Logic: EIP ← target
• EIP(指令指针)被设置为目标地址,从而将执行流转到目标标签。
EIP (Instruction Pointer) is set to the target address, which directs the flow of execution to the target label.
• Example:
top:
; Some code here
jmp top
• 在这个示例中,jmp top将控制流重新定向(redirect)到标签top,从而通过反复跳转到相同标签创建一个无限循环。
额外注意:
• 跳出当前过程需要使用特殊标签“全局标签”(global label),更多细节在第5.5.2.3节。
LOOP Instruction
• LOOP指令是一个基于ECX寄存器值的条件跳转指令。它会递减ECX的值,并在ECX不为零时继续循环。
The LOOP instruction is a conditional jump that creates a counting loop
LOOP target
• target 是控制流在满足条件时跳转的标签。
• 逻辑:
ECX ← ECX - 1:将ECX的值减1。
if ECX != 0, jump to target:如果ECX在递减后不为零,则跳转到指定的target标签。
• 汇编器计算LOOP指令之后的指令与target标签之间的字节距离,称为“相对偏移”(relative offset)。
• 将此偏移量(offset)加到EIP(指令指针)以在循环继续时将执行重定向(redirect)到target。
• EIP(指令指针) 是一个寄存器,用来保存“下一条要执行的指令”的地址。程序执行的每一步,EIP 的值都会变化,指向接下来的指令。
LOOP Example
calculation of the sum of integers (5 + 4 + 3 + 2 + 1):
mov ax, 0 ;将AX寄存器初始化为零,用于累加总和。
mov ecx, 5 ;设置ECX寄存器为5,表示循环将执行的次数。
L1: add ax, cx ;将当前CX的值(从5开始递减)加到AX中。
loop L1 ; 该指令递减ECX并在ECX不为零时跳转到L1。
loop L1 指令包含了这两个动作:递减 ECX 并判断是否跳转。
• 每次循环迭代(iteration)会减少CX并将其加到AX中,最终AX的值是5、4、3、2和1的总和。
• MOV ECX, 5 是为了设置一个32位的计数器,因为 LOOP 指令默认操作的是 ECX。不过在加法运算时,程序只关心 CX,所以用它就够了。
• ECX(32位寄存器)可以分解为以下几部分:
(1)CX(16位寄存器):即 ECX 的低16位部分。
(2)CH(高位8位):CX 的高8位。
(3)CL(低位8位):CX 的低8位。
• 跳转计算:
汇编器计算相对偏移量为-5 (FBh(这是-5在8位补码中的表示)),使得loop L1跳回到地址0x00000009。
计算这个跳转距离:
• 0x00000009(目标地址)减去 0x0000000C(当前地址) = -3 字节。
然而,LOOP 指令的偏移量计算会考虑自身指令的字节数,所以它内部会再减去2个字节(LOOP指令的长度)。因此,最终的偏移量是 -5 字节。
十四、Nested Loops
在编写嵌套循环时,需要保存外循环(outer loop)的ECX值。
Nested Loop Example
.data
count DWORD ?
.code
mov ecx, 100 ; 设置外循环计数
L1:
mov count, ecx ; 保存(save)外循环计数
mov ecx, 20 ; 设置内循环计数
L2:
; 内循环内容
loop L2 ; 重复内循环
mov ecx, count ; 恢复(restore)外循环计数
loop L1 ; 重复外循环
解释:在进入内层循环之前保存外层循环的计数,以便完成内层循环后可以恢复外层循环的计数。
十五、Summing an Integer Array
calculates the sum of an array of 16-bit integers.
.data
intarray WORD 100h, 200h, 300h, 400h
.code
mov edi, OFFSET intarray ; 数组地址
mov ecx, LENGTHOF intarray ; 循环计数 loop counter
mov ax, 0 ; 累加器清零 zero the accumulator
L1:
add ax, [edi] ; 添加整数
add edi, TYPE intarray ; 指向下一个整数
loop L1 ; 重复直到ECX = 0
数据段基址(Segment Base Address)是由系统在运行时设定的。程序访问 intarray 时,系统会将 OFFSET 返回的偏移量加上数据段基址,从而得到该变量的实际内存地址。
前缀 0x/后缀 h 用来表示十六进制数
[edi]:表示访问 EDI 所指向的内存地址的值,如果没有 [],就是直接使用寄存器的值
解释:该代码通过循环将数组中的所有整数累加起来,结果存储在AX中。
十六、Copying a String
The following code copies a string from source to target.
.data
source BYTE "This is the source string", 0 ;以 0 作为字符串的结束标记(类似于 C 语言中的字符串结尾 \0)
target BYTE SIZEOF source DUP(0) ; target 的字节数组长度与 source 字符串相同
.code
mov esi, 0 ; 索引寄存器,ESI 起始值为 0,即从字符串的第一个字符开始
mov ecx, SIZEOF source ; 循环计数
L1:
mov al, source[esi] ; 从source获取字符
mov target[esi], al ; 存入target,将 AL 中的字符存入 target 数组中 ESI 指定的位置
inc esi ; 移动到下一个字符
loop L1 ; 完整字符串复制
• SIZEOF:获取变量占用的总字节数。对于字符串 source,它返回字符串的总大小(字节数),包括每个字符所占的字节数。
• LENGTHOF:获取数组中元素的个数,不关心每个元素的大小。
如果 source 是一个 WORD 或 DWORD 类型数组,SIZEOF 会计算每个元素占用的字节数,从而得到实际的总字节大小。而 LENGTHOF 只返回数组中的元素数量。
解释:该代码逐字符将source字符串复制到target,利用循环和索引寄存器实现。
十七、Stack Operations
内存为CPU提供数据和指令存储,CPU从内存中读取数据并进行处理。
CPU寄存器 > CPU缓存(Cache) > 内存 > 磁盘存储(如硬盘、SSD)
• 当CPU需要执行一条指令或处理某个数据时,它会通过地址总线在内存中找到数据的位置,将该数据加载到自己的寄存器中进行处理。如果数据不在内存中,操作系统会从硬盘将数据调入内存,称为页面置换或内存分页。
使用堆栈来保存(save)和恢复(restore)寄存器的值,PUSH和POP指令按相反顺序执行(in reverse oder)。
栈本身并不是一块特定的内存区域,而是一种数据结构和管理机制
• 栈区域是指操作系统在内存(RAM)中为每个进程或线程分配的一块内存空间
运行时栈是内存中的一块特定区域(在栈区域中实现),这块内存专门用于管理函数调用、参数传递、返回地址和局部变量等临时数据。
(每次函数调用都会在运行时栈中创建一个“栈帧stack frame”,存储该函数的相关数据)
1. Runtime Stack 运行时栈
Imagine a stack of plates…
Plates(数据) are only added to the top
Plates are only removed from the top
LIFO structure(后进先出结构)保存返回地址和局部变量等数据。
运行时栈由CPU管理,使用两个寄存器
(1)SS (栈段)(stack segment): 标识栈所在的内存段
(2)ESP (栈指针)(stack pointer): 指向栈顶的当前位置,管理数据的存取
在实地址模式(real-address mode)下用SP代替ESP指针
2. PUSH Operation 推操作
将数据推入栈的操作
A 32-bit push operation:每次PUSH操作都会让ESP寄存器的值减小4(即向下移动4字节),腾出空间放置新的数据。
对于32位系统而言,通常一次操作会处理4字节的数据,因此在32位架构中常常增加4;而在16位模式下的操作通常增加2。
推入两个整数后的相同栈
栈向下增长(grows downward),意味着栈顶(ESP指针指向的地方)不断向内存的低地址移动。
ESP下方的区域始终可用(除非栈溢出overflowed)
3. POP Operation 弹操作
从栈顶取出数据的操作
将栈顶的数据(stack[ESP]的值)复制到寄存器或变量中,然后将ESP寄存器的值增加(向上移动)以移出刚弹出的数据位置。
• 增加值:ESP会增加n(2或4),具体取决于接收数据的操作数的属性(attribute)
• 如果数据是16位(2字节),那么ESP需要增加 2。
• 如果数据是32位(4字节),那么ESP需要增加 4。
4. PUSH and POP Instructions
• PUSH Syntax:
PUSH r/m16:将16位寄存器或内存操作数压入栈。
PUSH r/m32:将32位寄存器或内存操作数压入栈。
PUSH imm32:将32位立即数压入栈。
• POP syntax:
POP r/m16:从栈顶弹出16位数据到寄存器或内存操作数。
POP r/m32:从栈顶弹出32位数据到寄存器或内存操作数。
解释:PUSH指令用于将数据压入栈,POP指令用于从栈顶取出数据。这些指令支持16位和32位数据操作。
5. Using PUSH and POP
• 作用:保存和恢复寄存器中的重要值,以免在后续操作中被覆盖。
PUSH and POP instructioins occur in the opposite order
push esi ; 保存寄存器 esi 的当前值到栈中
push ecx ; 保存寄存器 ecx 的当前值到栈中
push ebx ; 保存寄存器 ebx 的当前值到栈中mov esi, OFFSET dwordVal ; 将 dwordVal 的内存偏移地址加载到 esi 中,指向数据位置
mov ecx, LENGTHOF dwordVal ; 将 dwordVal 的长度加载到 ecx 中,用于显示的字节数或元素数
mov ebx, TYPE dwordVal ; 将 dwordVal 的数据类型加载到 ebx 中,用于定义数据大小(如字节、字)
call DumpMem ; 调用 DumpMem 函数,显示或处理 dwordVal 数据pop ebx ; 从栈中恢复原先保存的 ebx 值
pop ecx ; 从栈中恢复原先保存的 ecx 值
pop esi ; 从栈中恢复原先保存的 esi 值
• 常规操作:CPU通常先将数据存储在寄存器,然后再从寄存器写入到内存。
解释:此代码块展示了如何使用PUSH和POP指令保护寄存器值,确保在函数调用或其他操作后可以恢复原始数据。
Example: PUSH and POP in Nested Loops
mov ecx, 100 ; 设置外循环计数 set outer loop counter
L1: ; 开始外循环 begin the outer loop
push ecx ; 保存外循环计数
mov ecx, 20 ; 设置内循环计数
L2: ; 开始内循环
; 内循环内容
loop L2 ; 重复内循环 repeat the inner loop
pop ecx ; 恢复外循环计数
loop L1 ; 重复外循环
如果将 loop L1 放在 pop ecx 的前面,每次循环都会先减 ecx 的值,再从栈中恢复 ecx 的值。
解释:在进入内循环前,使用PUSH保存外层循环的计数,并在退出内循环后使用POP恢复。
6. TEST Instruction
在执行完 TEST 指令后:
• 如果 结果为零(即所有对应位都为 0),零标志(ZF)会被设置为 1。
• 如果 结果不为零(即至少有一位为 1),零标志(ZF)会被清除为 0。
• 作用:执行两个操作数的位与操作(AND operation),但不改变操作数本身,仅影响零标志(Zero Flag,ZF)。
• 示例 1:检查 AL 寄存器的第 0 位或第 1 位是否为1
test al, 00000011b ; 对 AL 寄存器的低两位执行 AND 操作
jnz ValueFound ; 如果结果非零(至少一位为1),跳转到 ValueFound 标签
当指令的执行结果不是零时,零标志(ZF)不会被置为1,此时我们称零标志未设置
• 示例 2:检查 AL 寄存器的第 0 位和第 1 位是否均为0
test al, 00000011b ; 对 AL 寄存器的低两位执行 AND 操作
jz ValueNotFound ; 如果结果为零(两位都为0),跳转到 ValueNotFound 标签
解释:test 指令用于条件判断,不会改变操作数的值。只会影响零标志,以便后续跳转指令判断条件。
7. CMP Instruction
• 作用:比较目标操作数(destination)和源操作数(source)的大小,不会修改操作数,只会影响标志位。
CMP destination, source
• 示例 1:检查destination==source
mov al, 5 ; 将 5 赋值给 AL
cmp al, 5 ; 比较 AL 和 5
; Zero flag (ZF) = 1,CF = 0(没有借位)
• 示例 2:检查destination < source
mov al, 4 ; 将 4 赋值给 AL
cmp al, 5 ; 比较 AL 和 5
; ZF = 0(不相等), Carry flag (CF) = 1
jb LessThan ; 如果 CF 被设置(即 AL < 5),跳转到 LessThan
进位标志(Carry Flag, CF) 用于表示无符号数的比较结果
• 如果目标操作数小于源操作数(无符号数比较),减法会产生“借位”,因为目标操作数不足以减去源操作数。这时,进位标志(CF)会被设置为 1,表示目标小于源。
解释:CMP 指令通过减法操作影响标志位,用于后续条件跳转。零标志、进位标志等都会根据比较结果设置。
• 示例:检查destination > source。
mov al, 6 ; 将 6 赋值给 AL
cmp al, 5 ; 比较 AL 和 5
; Zero flag (ZF) = 0, Carry flag (CF) = 0
解释:当目标操作数大于源操作数时,零标志和进位标志都被清除(设为 0)。
CMP Instruction (有符号signed整数比较)
• 有符号数比较:比较有符号整数时会根据符号标志(Sign Flag, SF)和溢出标志(Overflow Flag, OF)的关系判断大小。
• 示例 1:destination > source
mov al, 5 ; 将 5 赋值给 AL
cmp al, -2 ; 比较 AL 和 -2
; SF(Sign Flag) = OF(Overflow Flag)
• 示例 2:destination < source
mov al, -1 ; 将 -1 赋值给 AL
cmp al, 5 ; 比较 AL 和 5
; SF(Sign Flag) != OF(Overflow Flag)
-1 - 5结果会是一个负数,因此会设置符号标志(SF),即 SF = 1
• 因为 -1 和 5 的比较在有符号数范围内,不存在溢出,所以 溢出标志(OF)被清除为 0。
溢出标志(OF) 用于判断有符号数的运算结果是否超出了表示范围。溢出发生的条件是:在有符号数加法或减法运算中,结果超出了操作数的数据范围
标志寄存器是一个用于存储CPU运算结果状态信息的寄存器,它包含多个标志位(Flag Bits),每个位用于表示特定的条件或状态
解释:对于有符号数的比较,SF 和 OF 的关系用于判断比较结果。
8. Conditional Jumps(条件跳转)
• Jumps Based on:
特定标志(Specify Flags):如进位标志、零标志等,用于不同的跳转条件。
等值(Equality):跳转用于判断相等情况。
无符号比较(Unsigned Comparisons):用于无符号数比较。
有符号比较(Signed Comparisons):用于有符号数比较。
• Application:
条件跳转在数据比较、分支和循环控制等场景中广泛应用。
例如,加密字符串(Encrypting a String)、位测试指令(Bit Test, BT)
解释:条件跳转是基于不同标志和比较结果执行分支操作的关键指令。
9. Jcond Instruction
• 条件跳转指令:根据特定的寄存器或标志条件执行跳转。
常见跳转指令:
• JB, JC - 如果进位标志 (Carry Flag, CF) 被设置(set),跳转。
• JE, JZ - 如果零标志 (Zero Flag, ZF) 被设置,跳转。
• JS - 如果符号标志 (Sign Flag, SF) 被设置,跳转。
• JNE, JNZ - 如果零标志 (ZF) 未设置(clear),跳转。
• JECXZ - 如果 ECX 寄存器为 0,跳转。
解释:Jcond 指令根据特定的标志或寄存器状态来决定是否跳转,用于实现条件分支和循环控制。
Jcond Ranges
• 在 386 处理器之前:条件跳转(Jcond)指令的跳转范围限制在当前地址的 -128 到 +127 字节以内。这意味着只能在较短的距离内进行条件跳转。
• x86 处理器:支持 32 位偏移(offset),允许跳转到内存中的任何位置,从而支持更远距离的跳转。
基于特定标志的跳转(Jumps Based on Specific Flags)
基于相等的跳转(Jumps Based on Equality)
基于无符号比较的跳转(Jumps Based on Unsigned Comparison)
• 适用于无符号数的条件跳转指令:
第五张图片 - 基于有符号比较的跳转(Jumps Based on Signed Comparison)
• 适用于有符号数的条件跳转指令:
条件跳转应用(Applications)
任务: 当无符号数 EAX 大于 EBX 时跳转到 Larger 标签。
cmp eax, ebx
ja Larger
任务: 当有符号数 EAX 大于 EBX 时跳转到 Greater 标签。
cmp eax, ebx
jg Greater
小于或等于的跳转
任务: 当无符号数 EAX 小于或等于 Val1 时跳转到标签 L1。
cmp eax, Val1
jbe L1
任务: 当有符号数 EAX 小于或等于 Val1 时跳转到标签 L1。
cmp eax, Val1
jle L1
比较并复制较大或较小的值
1. 无符号数比较: 比较 AX 和 BX 的大小,将较大的值复制到 Large。
mov Large, bx
cmp ax, bx
jna Next
mov Large, ax
Next:
2. 有符号数比较: 比较 AX 和 BX,将较小的值复制到 Small。
mov Small, ax
cmp bx, ax
jnl Next
mov Small, bx
Next:
内存比较与偶数判断
1. 内存比较: 如果 ESI 指向的内存值为 0,则跳转到 L1 标签。
使用 CMP 比较 [ESI] 和 0,然后使用 JE(跳转等于)指令。
cmp WORD PTR [esi], 0
je L1
PTR(指针):
• 表示“指针类型声明”。
• 它的主要作用是告诉编译器或汇编器如何解释该内存地址中的数据类型,即数据的长度或宽度。
• 加入 PTR 后,明确告诉汇编器,WORD 是针对 [esi] 这个内存地址的操作。
• [esi] 指向的是该内存地址中的数据。
偶数检查: 检查 EDI 指向的双字是否为偶数,若为偶数则跳转到 L2。
• 解决方法: 使用 TEST 指令检查最低位是否为 0将 [edi] 的最低有效位与掩码 1 按位与&,然后使用 JZ(跳转零)跳转。
• 代码示例:
test DWORD PTR [edi], 1
jz L2
• 偶数的最低有效位始终为 0(如 ...000, ...010)。
• 奇数的最低有效位始终为 1(如 ...001, ...011)。
图片 5: 位检查跳转
1. 任务: 如果 AL 寄存器中的位 0、1 和 3 都被设置,则跳转到标签 L1。
• 解决方法: 使用 AND 指令清除其他位,仅保留 0、1 和 3 位,接着用 CMP 比较结果,如果相等则跳转。
and al, 00001011b ; 清除其他位,仅保留 0、1 和 3
cmp al, 00001011b ; 检查这些位是否都被设置
je L1 ; 如果相等,则跳转到 L1
and 指令:
• 按位与操作:将 AL 寄存器中的值和 00001011b(二进制)按位与。
• 掩码 00001011b 的作用是保留 AL 的第 0、1 和 3 位(这些位需要检查),其他位会被清零。
十八、Conditional Loop Insruction
1. LOOPZ and LOOPE
Syntax:
LOOPE destination
LOOPZ destination
Logic: ECX <-- ECX – 1 if ECX > 0 and ZF=1, jump to destination
• 遍历数组,找到第一个不匹配特定值的元素时结束
2. LOOPNZ and LOOPNE
Syntax:
LOOPNZ destination
LOOPNE destination
Logic: ECX <-- ECX – 1; if ECX > 0 and ZF=0, jump to destination
• 用于遍历数组,找到第一个匹配特定值的元素时结束。
LOOPNZ Example
finds the first positive value in an array
.data
array SWORD -3, -6, -1, -10, 10, 30, 40, 4 ; 定义一个数组
sentinel SWORD 0 ; 定义哨兵值.code
mov esi, OFFSET array ; 将数组的地址加载到 ESI
mov ecx, LENGTHOF array ; 将数组长度加载到 ECXnext:
test WORD PTR [esi], 8000h ; 测试最高位 (符号位)
pushfd ; 保存标志寄存器
add esi, TYPE array ; 递增 ESI 到下一个元素
popfd ; 恢复标志寄存器
loopnz next ; 如果符号位不为负,继续循环
jnz quit ; 如果没有找到正数,退出循环
sub esi, TYPE array ; 将 ESI 指回找到的值
quit:
sub esi, TYPE array
• 数组元素是 SWORD 类型(2字节)。
• 初始时,ESI 指向数组第一个元素的地址。
• 每次跳到下一个元素,ESI 会增加 2(因为 TYPE array = 2)。
当循环找到正值 10 时,ESI 已经跳到了 30 的位置。
• 减去 2,让 ESI 回退到 10 的位置。
如果 array 是 WORD 类型(2字节),则 TYPE array = 2
(1)test WORD PTR [esi], 8000h: 8000h 是一个掩码,二进制为 1000 0000 0000 0000,用于提取 16 位数的最高位(第 15 位)。
(2) 在循环或分支操作中,某些指令会修改标志寄存器的内容(如 test、cmp 等)。
通过 pushfd 和 popfd,可以保护原标志位状态因为add 指令会修改标志寄存器(如 ZF 和 CF)
(3)cld
• 作用:清除方向标志位 (DF, Direction Flag)。
• DF 位在 EFLAGS 寄存器中,控制字符串处理指令(如 rep, cmpsd, stosd 等)是递增模式还是递减模式。
cmpsd(Compare Strings Doubleword):
• 比较 ESI 和 EDI 指向的 4 字节(DWORD)数据。
• 如果相等,更新标志寄存器并递增 ESI 和 EDI,同时 ECX 减 1。
• 如果不相等,则退出循环。
add 指令
是算术运算,可能影响多个标志位:
• ZF(零标志):加法结果为零时,ZF = 1,否则为 0。
• CF(进位标志):结果是否产生进位。
• OF(溢出标志):加法结果是否溢出。
• SF(符号标志):结果是否为负。
标志寄存器(EFLAGS)
• 标志寄存器 是 CPU 中用于存储条件标志和控制标志的寄存器,它反映了上一次指令的结果状态。
• 常见的标志位:
• ZF(零标志):运算结果是否为零。
• CF(进位标志):是否产生进位或借位。
• SF(符号标志):结果是否为负值。
• OF(溢出标志):运算是否产生溢出。
• PF(奇偶标志):结果是否为偶数个 1。
• AF(辅助进位标志):用于十进制调整。
• DF(方向标志):字符串操作的方向。
Comparing Arrays
.data
source DWORD COUNT DUP(?)
target DWORD COUNT DUP(?)
mov ecx, COUNT ; 将数组的长度加载到 ECX 中,作为循环计数器
mov esi, OFFSET source ; 将源数组的起始地址加载到 ESI 中
mov edi, OFFSET target ; 将目标数组的起始地址加载到 EDI 中
cld ; 清除方向标志位(DF=0),设置为递增模式
repe cmpsd ; 按双字比较两个数组的元素,遇到不相等时停止比较
count
如果没有定义 COUNT,代码将无法正常汇编。
• 它控制数组的长度,用于动态调整数组的大小。
repe cmpsd
• repe (Repeat While Equal)(是前缀指令,表示 “重复执行直到比较结果不相等” 或 “计数器 (ecx) 减为零”
• 内部工作机制如下:
1. 比较当前元素:
• 比较源数组([ESI] 指向的值)和目标数组([EDI] 指向的值)。
• 如果相等,则继续循环;否则退出。
2. 偏移自动更新:
• 每次执行后,ESI 和 EDI 都会自动根据方向标志位(DF)的设置,增加或减少。
• 若 DF = 0(默认使用 cld 设置为递增),ESI 和 EDI 的偏移量都会增加 4(因为 cmpsd 操作的是 DWORD,即 4 字节)。
• 若 DF = 1(方向标志位为递减),偏移量会减少 4。
3. 循环计数:
• 每次比较后,ECX 寄存器会减 1,作为剩余比较次数的计数器。
• 若 ECX > 0 且 ZF = 1(比较结果相等),继续循环;否则退出。
cld(Clear Direction Flag)
设置方向标志位为递增模式(正向遍历)
汇编语言操作限制
• 汇编指令(例如 cmp、repe cmpsd 等)通常需要操作单个元素或地址,无法直接操作整个数组或字符串内容。
Example: Comparing Two Strings
It displays a message indicating whether the lexical value of the source string is less than the destination string.
.data
source BYTE "MARTIN " ; 定义源字符串
dest BYTE "MARTINEZ" ; 定义目标字符串
str1 BYTE "Source is smaller", 0dh, 0ah, 0 ; 输出信息: 源字符串较小
str2 BYTE "Source is not smaller", 0dh, 0ah, 0 ; 输出信息: 源字符串不较小
.code
main PROC
cld ; 清除方向标志位,设置为递增模式
mov esi, OFFSET source ; 加载源字符串的起始地址到 ESI
mov edi, OFFSET dest ; 加载目标字符串的起始地址到 EDI
mov ecx, LENGTHOF source ; 将源字符串的长度加载到 ECX 中
repe cmpsb ; 逐字节比较源字符串和目标字符串,直到不相等或长度为 0
jb source_smaller ; 如果源字符串小于目标字符串,跳转到 source_smaller 标签
mov edx, OFFSET str2 ; 设置结果为 "Source is not smaller"
jmp done ; 跳转到 done 标签,结束程序
source_smaller: ; 标签:当源字符串小于目标字符串时
mov edx, OFFSET str1 ; 设置结果为 "Source is smaller"
done: ; 标签:程序结束处理
call WriteString ; 输出比较结果
exit ; 退出程序
main ENDP
END main
CMPSB 指令
• 按字节比较两个操作数,结果影响标志寄存器(如 ZF)。
0dh 和 0ah 是十六进制数
• 0dh (13 in decimal): 表示回车(Carriage Return,CR)。
• 0ah (10 in decimal): 表示换行(Line Feed,LF)。
• 0 (Null): 表示字符串的结束标记(null terminator),通常用来表示字符串的终止。
经常用来格式化输出
str1 BYTE "Source is smaller", 0dh, 0ah, 0
这表示一个字符串 "Source is smaller",其后跟着回车、换行,最后以 0 结束。
PROC 是汇编程序中的过程声明(Procedure Declaration)
• main 是过程的名称,通常代表程序的入口点。
• 汇编中的过程类似于高层编程语言中的函数或方法,定义了一段可以重复调用的代码。
• 在标准程序中,main 通常是程序执行的起点(与 C/C++ 的 main() 类似)。
main PROC
; 这里写主程序的代码
main ENDP
• main ENDP 表示过程的结束。
WriteString
• WriteString 是一个子例程的名称,用来将字符串输出到屏幕上。
• 它通常是由程序库提供的函数,比如 Irvine32 汇编库中就有这个功能。
• 在使用 WriteString 时,需要事先将字符串的地址加载到指定寄存器中(如 EDX),然后调用该子例程。
END main
• END 指令:表示程序的结束。
• 告诉编译器程序在此终止。
• 但同时,它也指定程序的入口点(即程序从哪里开始执行)。
• main 是入口点:
• END main 表示程序的入口点是 main 过程。
• 当程序被加载到内存中时,执行从 main 开始。
main PROC
; 主程序代码
main ENDP
END main
这表明程序从 main 的起始处开始执行。
Sorting Integer Arrays
Bubble Sort
• Bubble Sort 是一种简单的排序算法,适合小型数组。
• 算法思路:
比较相邻的两个值,如果顺序错误就交换。
每一轮操作后,最大的值“冒泡”到数组末尾。
Bubble Sort Pseudocode
cx1 = N - 1 // 外层循环,控制未排序部分的长度
while (cx1 > 0) {
esi = addr(array) // 初始化当前数组地址
cx2 = cx1 // 内层循环,控制元素的比较次数
while (cx2 > 0) {
if (array[esi] < array[esi + 4]) { // 比较相邻两个值
exchange(array[esi], array[esi + 4]) // 交换
}
add esi, 4 // 指向下一个元素
dec cx2 // 减少内层循环计数
}
dec cx1 // 减少外层循环计数
}
• 外层循环通过 cx1 控制未排序部分。
• 内层循环通过 cx2 比较每一轮中相邻的元素。
addr(array) 是一种 伪代码(pseudo-code),它并不是汇编语言中的真实指令,而是一种高层次的、简化表达的方式,用来描述程序的逻辑或操作步骤。
在实际的汇编语言中,addr(array) 并不存在,而是用类似的实际指令(如 lea 或 mov OFFSET)来实现其效果。
实际汇编:lea esi, array(将 array 的内存地址加载到 esi 寄存器中)。
Bubble Sort Implementation
BubbleSort PROC USES eax ecx esi,
pArray:PTR DWORD, Count:DWORD
mov ecx, Count ; 将数组长度加载到 ECX 中
dec ecx ; 外层循环次数为 Count - 1
L1: push ecx ; 保存外层循环计数器
lea esi, pArray ; 将数组的起始地址加载到 ESI
L2: mov eax, [esi] ; 加载当前元素到 EAX
cmp [esi + 4], eax ; 比较相邻的两个值
jge L3 ; 如果顺序正确,跳过交换
xchg eax, [esi + 4] ; 否则交换两个值
mov [esi], eax ; 更新交换后的值
L3: add esi, 4 ; 指向下一个元素
loop L2 ; 内层循环:继续比较
pop ecx ; 恢复外层循环计数器
loop L1 ; 外层循环:缩小未排序部分
L4: ret ; 返回
BubbleSort ENDP
• pArray 是一个指向 DWORD 数据的指针,通常用于表示一个数组的起始地址。
• Count 是一个整数,通常用来表示某个数组的长度或者计数器。
例子:
如果 Count = 4,则表示数组 pArray 中有 4 个元素。
jge 是 Jump if Greater or Equal
ENDP 是汇编语言(特别是 MASM 和 TASM 汇编器)中的伪指令,用来标记一个过程(PROC)的结束。