Win32汇编:数组与标志位测试总结

整理复习汇编语言的知识点,以前在学习《Intel汇编语言程序设计 - 第五版》时没有很认真的整理笔记,主要因为当时是以学习理解为目的没有整理的很详细,这次是我第三次阅读此书,每一次阅读都会有新的收获,这次复习,我想把书中的重点,再一次做一个归纳与总结(注:16位汇编部分跳过),并且继续尝试写一些有趣的案例,这些案例中所涉及的指令都是逆向中的重点,一些不重要的我就直接省略了,一来提高自己,二来分享知识,转载请加出处,敲代码备注挺难受的。

本次复习重点在于理解数组中常用的寻址方式以及标志位的测试命令,数组寻址包括了,直接寻址,间接寻址,立即数寻址,基址变址寻址,比例因子寻址,通过ESI内存寻址,通过ESP堆栈寻址,指针寻址。

再次强调:该笔记主要学习的是汇编语言,不是研究编译特性的,不会涉及到编译器的优化与代码还原。


数组取值操作符: 数组取值操作符是对数组操作之前必须要掌握的,以下命令主要实现对数组元素的统计,取偏移值等,后期数组寻址会用到.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
WordVar1 WORD 1234h
DwordVar2 DWORD 12345678h

ArrayBT BYTE 1,2,3,4,5,6,7,8,9,0h
ArrayDW DWORD 1000,2000,3000,4000,5000,6000,7000,8000,9000,0h
ArrayTP DWORD 30 DUP(?)
.code

main PROC
; 使用 OFFSET 可返回数据标号的偏移地址,单位是字节.
; 偏移地址代表标号距DS数据段基址的距离.
xor eax,eax
mov eax,offset WordVar1
mov eax,offset DwordVar2

; 使用 PTR 可指定默认取出参数的大小(DWORD/WORD/BYTE)
mov eax,dword ptr ds:[DwordVar2] ; eax = 12345678h
xor eax,eax
mov ax,word ptr ds:[DwordVar2] ; ax = 5678h
mov ax,word ptr ds:[DwordVar2 + 2] ; ax = 1234h

; 使用 LENGTHOF 可以计算数组元素的数量
xor eax,eax
mov eax,lengthof ArrayDW ; eax = 10
mov eax,lengthof ArrayBT ; eax = 10

; 使用 TYPE 可返回按照字节计算的单个元素的大小.
xor eax,eax
mov eax,TYPE WordVar1 ; eax = 2
mov eax,TYPE DwordVar2 ; eax = 4
mov eax,TYPE ArrayDW ; eax = 4

; 使用 SIZEOF 返回等于LENGTHOF(总元素数)和TYPE(每个元素占用字节)返回值的乘基.
xor eax,eax
mov eax,sizeof ArrayBT ; eax = 10
mov eax,sizeof ArrayTP ; eax = 120

invoke ExitProcess,0
main ENDP
END main

数组直接寻址: 在声明变量名称的后面加上偏移地址即可实现直接寻址,直接寻址中可以通过立即数寻址,也可以通过寄存器相加的方式寻址,如果遇到双字等还可以使用基址变址寻址,这些寻址都属于直接寻址.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
ArrayB BYTE 10h,20h,30h,40h,50h
ArrayW WORD 100h,200h,300h,400h
ArrayDW DWORD 1h,2h,3h,4h,5h,6h,7h,8h,9h
.code
main PROC
; 针对字节的寻址操作
mov al,[ArrayB] ; al=10
mov al,[ArrayB+1] ; al=20
mov al,[ArrayB+2] ; al=30

; 针对内存单元字存储操作
mov bx,[ArrayW] ; bx=100
mov bx,[ArrayW+2] ; bx=200
mov bx,[ArrayW+4] ; bx=300

; 针对内存单元双字存储操作
mov eax,[ArrayDW] ; eax=00000001
mov eax,[ArrayDW+4] ; eax=00000002
mov eax,[ArrayDW+8] ; eax=00000003

; 基址加偏移寻址: 通过循环eax的值进行寻址,每次eax递增2
mov esi,offset ArrayW
mov eax,0
mov ecx,lengthof ArrayW
s1:
mov dx,word ptr ds:[esi + eax]
add eax,2
loop s1

; 基址变址寻址: 循环取出数组中的元素
mov esi,offset ArrayDW ; 数组基址
mov eax,0 ; 定义为元素下标
mov ecx,lengthof ArrayDW ; 循环次数
s2:
mov edi,dword ptr ds:[esi + eax * 4] ; 取出数值放入edi
inc eax ; 数组递增
loop s2

invoke ExitProcess,0
main ENDP
END main

数组间接寻址: 数组中没有固定的编号,处理此类数组唯一可行的方法是用寄存器作为指针并操作寄存器的值,这种方法称为间接寻址,间接寻址通常可通过ESI实现内存寻址,也可通过ESP实现对堆栈的寻址操作.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
ArrayDW DWORD 1h,2h,3h,4h,5h,6h,7h,8h,9h
.code
main PROC
; 第一种: 通过使用ESI寄存器实现寻址.
mov esi,offset ArrayDW ; 取出数组基地址
mov ecx,lengthof ArrayDW ; 取出数组元素个数
s1:
mov eax,dword ptr ds:[esi] ; 间接寻址
add esi,4 ; 每次递增4
loop s1

; 第二种: 通过ESP堆栈寄存器,实现寻址.
mov eax,100 ; eax=1
mov ebx,200 ; ebx=2
mov ecx,300 ; ecx=3
push eax ; push 1
push ebx ; push 2
push ecx ; push 3

mov edx,[esp + 8] ; EDX = [ESP+8] = 1
mov edx,[esp + 4] ; EDX = [ESP+4] = 2
mov edx,[esp] ; EDX = [ESP] = 3

; 第三种(高级版): 通过ESP堆栈寄存器,实现寻址.
push ebp
mov ebp,esp ; 保存栈地址
lea eax,dword ptr ds:[ArrayDW] ; 获取到ArrayDW基地址
; -> 先将数据压栈
mov ecx,9 ; 循环9次
s2: push dword ptr ss:[eax] ; 将数据压入堆栈
add eax,4 ; 每次递增4字节
loop s2
; -> 在堆栈中取数据
mov eax,32 ; 此处是 4*9=36 36 - 4 = 32
mov ecx,9 ; 循环9次
s3: mov edx,dword ptr ss:[esp + eax] ; 寻找栈中元素
sub eax,4 ; 每次递减4字节
loop s3

add esp,36 ; 用完之后修正堆栈
pop ebp ; 恢复ebp

invoke ExitProcess,0
main ENDP
END main

比例因子寻址: 通过使用比例因子,以下例子每个DWORD=4字节,且总元素下标=0-3,得出比例因子3* type arrayDW.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
ArrayW WORD 1h,2h,3h,4h,5h
ArrayDW DWORD 1h,2h,3h,4h,5h,6h,7h,8h,9h

TwoArray DWORD 10h,20h,30h,40h,50h
RowSize = ($ - TwoArray) ; 每行所占空间 20 字节
DWORD 60h,70h,80h,90h,0ah
DWORD 0bh,0ch,0dh,0eh,0fh
.code
main PROC

; 第一种比例因子寻址
mov esi,0 ; 初始化因子
mov ecx,9 ; 设置循环次数
s1:
mov eax,ArrayDW[esi * 4] ; 通过因子寻址,4 = DWORD
add esi,1 ; 递增因子
loop s1

; 第二种比例因子寻址
mov esi,0
lea edi,word ptr ds:[ArrayW]
mov ecx,5
s2:
mov ax,word ptr ds:[edi + esi * type ArrayW]
inc esi
loop s2

; 第三种二维数组寻址
row_index = 1
column_index = 2

mov ebx,offset TwoArray ; 数组首地址
add ebx,RowSize * row_index ; 控制寻址行
mov esi,column_index ; 控制行中第几个
mov eax, dword ptr ds:[ebx + esi * TYPE TwoArray]

invoke ExitProcess,0
main ENDP
END main

通过比例因子可以模拟实现二维数组寻址操作,过程也很简单.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
TwoArray DWORD 10h,20h,30h,40h,50h
RowSize = ($ - TwoArray) ; 每行所占空间 20 字节
DWORD 60h,70h,80h,90h,0ah
DWORD 0bh,0ch,0dh,0eh,0fh
.code
main PROC
lea esi,dword ptr ds:[TwoArray] ; 取基地址
mov eax,0 ; 控制外层循环变量
mov ecx,3 ; 外层循环次数
s1:
push ecx ; 保存外循环次数
push eax

mov ecx,5 ; 内层循环数
s2: add eax,4 ; 每次递增4
mov edx,dword ptr ds:[esi + eax] ; 定位到内层循环元素
loop s2

pop eax
pop ecx
add eax,20 ; 控制外层数组
loop s1

invoke ExitProcess,0
main ENDP
END main

通过比例因子实现对数组求和操作,代码如下:

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
ArrayA DWORD 10h,20h,30h,40h,50h
ArrayB DWORD 10h,20h,30h,40h,50h
NewArray DWORD 5 dup(0)
.code
main PROC
; 循环让数组中的每一个数加10后回写
mov ebx,0
mov ecx,5
s1:
mov eax,dword ptr ds:[ArrayA + ebx * 4]
add eax,10
mov dword ptr ds:[ArrayA + ebx * 4],eax
inc ebx
loop s1

; 循环让数组A与数组B相加后赋值到数组NewArray
mov ebx,0
mov ecx,5
s2:
mov esi,dword ptr ds:[ArrayA + ebx]
add esi,dword ptr ds:[ArrayB + ebx]
mov dword ptr ds:[NewArray + ebx],esi
add ebx,4
loop s2

invoke ExitProcess,0
main ENDP
END main

数组指针寻址: 变量地址的变量称为指针变量(pointer variable),Intel处理器使用两种基本类型的指针,即near(近指针)和far(远指针),保护模式下使用Near指针,所以它被存储在双字变量中.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
ArrayA WORD 1h,2h,3h,4h,5h
ArrayB DWORD 1h,2h,3h,4h,5h

PtrA DWORD offset ArrayA ; 指针 PtrA --> ArrayA
PtrB DWORD offset ArrayB ; 指针 PTRB --> ArrayB
.code
main PROC

mov ebx,0 ; 寻址因子
mov ecx,5 ; 循环次数
s1:
mov esi,dword ptr ds:[PtrA] ; 将指针指向PtrA
mov ax,word ptr ds:[esi + ebx * 2] ; 每次递增2字节

mov esi,dword ptr ds:[PtrB] ; 将指针指向PtrB
mov eax,dword ptr cs:[esi + ebx * 4] ; 每次递增4字节
inc esi ; 基地址递增
inc ebx ; 因子递增
loop s1

invoke ExitProcess,0
main ENDP
END main

常见标志位测试: 标志寄存器又称程序状态寄存器,其主要用于存放条件码标志,控制标志和系统标志的寄存器,标志寄存器中存放的有条件标志,也有控制标志,这些标志则会影响跳转的实现,逆向中常见的标志位有如下几种.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
main PROC
; CF 进位标志位: 当执行一个加法(或减法)运算,使最高位产生进位(或借位)时,CF为1;否则为0
mov ax,0ffffh
add ax,1 ; cf = 1 af = 1

; PF 奇偶标志位: 当运算结果中,所有bit位(例:1001010)中1的个数为偶数时,则PF=1;为基数PF=0
mov eax,00000000b
add eax,00000111b ; pf = 0

mov eax,00000000b
add eax,00000011b ; pf = 1

; ZF 零标志位: 若当前的运算结果为零,则ZF=1; 否则ZF=0
mov eax,2
sub eax,2 ; zf = 1 cf = 0 af = 0

; SF 符号标志位: 若运算结果为负数,则SF=1;若为非负数则SF=0
mov eax,3e8h
sub eax,3e9h ; sf = 1 cf = 1 af = 1 zf = 0

; DF 方向标志位: 当DF=0时为正向传送数据(cld),否则为逆向传送数据(std)
cld
mov eax,1 ; df = 0
std
mov eax,1 ; df = 1

; OF 溢出标志位: 记录是否产生了溢出,当补码运算有溢出时OF=1;否则OF=0
mov al,64h
add al,64h ; of = 1 cf = 0 pf = 0 af = 0

invoke ExitProcess,0
main ENDP
END main

TEST 位与指令: 该指令在对操作数之间执行隐含与运算操作,并设置相应的标志位,与AND指令唯一的不同在于,该指令只会设置相应的标志,并不会替换目的操作数中的数值,常用于测试某些位是否被设置.

TEST指令可以同时检测设置多个标志位的值,该指令执行时总是清除溢出标志和进位标志,它修改符号标志,基偶标志,零标志的方式与AND指令相同.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
main PROC
mov al,00001111b
test al,2 ; zf=0 pf=0

mov al,00100101b
test al,00001001b ; zf=0 pf=0

mov al,00100100b
test al,00001001b ; zf=1 pf=1

mov eax,0100h
test eax,eax ; zf=0

mov eax,0
test eax,eax ; zf=0

or al,80h ; 设置符号标志 zf=0 pf=0 sf=1
and al,7fh ; 清除符号标志 zf=1 pf=1 sf=0

mov al,0
or al,1 ; 清除符号标志 zf=0 pf=0

stc ; 设置进位标志 cf = 1
clc ; 清除进位标志 cf = 0

mov al,07fh ; AL = +127
inc al ; 设置溢出标志 AL = 80h (-128) of=1 af=1 sf=1
or eax,0 ; 清除溢出标志

invoke ExitProcess,0
main ENDP
END main

CMP 比较指令: 该指令作用是在源操作数和目的操作数中间执行隐含的减法运算,两个操作数都不会被修改,仅会影响标志位的变化,CMP指令是高级语言实现程序逻辑的关键,也是汇编中非常重要的指令常与跳转指令合用.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
main PROC
; 比较5和10
mov ax,5
cmp ax,10 ; 5-10 > zf=0 cf=1 pf=0 af=1 sf=1

; 比较两个相同数
mov ax,1000
mov cx,1000
cmp cx,ax ; 1000-1000 > zf=1 cf=0 pf=1 sf=0

; 比较100和0
mov ax,100
cmp ax,0 ; 100-0 > zf=0 cf=0 pf=0

; 比较100和50
mov eax,100
mov ebx,50
cmp eax,ebx ; 100-50 > zf=0 cf=0 pf=0

; 比较-100和50
mov eax,-100
mov ebx,50
cmp eax,ebx ; -100-50 > sf=1 pf=1

; 比较-100和-50
mov eax,-100
mov ebx,-50
cmp eax,ebx ; -100--50 > cf=1 af=1 pf=0

invoke ExitProcess,0
main ENDP
END main

标志跳转指令: 跳转指令分为多种,第一种常见的跳转指令就是基于特定CPU的标志寄存器来实现的跳转形式.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
main PROC
; JZ/JE 当ZF置1时 也就是结果为零则跳转 (leftOp - rightOp = 0)
mov eax,1
sub eax,1 ; zf=1 pf=1
je jump

mov eax,1
mov ebx,1
cmp eax,ebx ; zf=1
jz jump

; JNZ/JNE 当ZF置0时 也就是结果不为零则跳转 (leftOp - rightOp != 0)
mov eax,2
sub eax,1
jnz jump ; zf=0 pf=0

mov eax,2
cmp eax,1
jne jump ; zf=0

; JC/JNC 当 CF=1/0 设置进位标志则跳/未设置进位标志则跳
mov al,0
cmp al,1
jc jump
jnc jump

; JO/JNO 当 OF=1/0 设置溢出标志则跳/未设置溢出标志则跳
mov al,0ffh
add al,1
jo jump

; JS/JNS 当 SF=1/0 设置符号标志则跳/未设置符号标志则跳
mov eax,1
cmp eax,1
js jump ; cf=0 af=0

; JP/JNP PF=1/0 设置奇偶标志则跳(偶)/未设置奇偶标志则跳(基)
mov al,00100100b
cmp al,0
jp jump ; zp=0
jump:
xor eax,eax
xor ebx,ebx

invoke ExitProcess,0
main ENDP
END main

比较跳转标志: 通过使用cmp eax,ebx比较等式两边的值,影响相应的标志寄存器中的值,从而决定是否要跳转,常用的如下:

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
main PROC
; JA(无符号)/JG(有符号) 跳转标志: (left > right) 大于则跳转
mov eax,100
mov ebx,200
cmp ebx,eax ; 无符号 ebx > eax
ja jump ; zf=0 pf=0

mov eax,20
mov ebx,-100
cmp eax,ebx ; 有符号 eax > ebx
jg jump ; zf=0 cf=1 pf=1 af=1

; JAE(无符号)/JGE(有符号) 跳转标志: (left >= right) 大于或等于则跳转
mov eax,50
mov ebx,20
cmp eax,ebx ; 无符号 eax >= ebx
jae jump ; jae 被替换成了jnb 小于则跳 (eax<ebx)

mov eax,50
mov ebx,-20
cmp eax,ebx ; 有符号 eax >= ebx
jge jump ; zf=0 af=1 pf=0 sf

; JB(无符号)/JL(有符号) 跳转标志:(left < right) 小于则跳转
mov eax,10
mov ebx,20
cmp eax,ebx ; 无符号 eax < ebx
jb jump ; cf=0 af=0 pf=1

mov eax,-10
mov ebx,20
cmp eax,ebx ; 有符号 eax < ebx
jl jump

; JBE(无符号)/JLE(有符号) 跳转标志:(left <= right) 小于或等于则跳转
mov eax,20
mov ebx,20
cmp eax,ebx ; 无符号 eax <= ebx
jbe jump ; zf=1

mov eax,-20
mov ebx,10
cmp eax,ebx ; 无符号 eax,ebx
jle jump ; sf=1

; JNB(不小于则跳 同JAE)/JNA(不大于则跳 同JBE) 跳转标志:(lef !>< right) 无符号
mov eax,10
mov ebx,5
cmp eax,ebx ; eax !< ebx
jnb jump

mov eax,5
mov ebx,10
cmp eax,ebx ; eax !> ebx
jbe jump

; JNB(不小于则跳 同JAE)/JNA(不大于则跳 同JBE) 跳转标志:(lef !>< right) 无符号
mov eax,10
mov ebx,5
cmp eax,ebx ; eax !< ebx
jnb jump ; zf=0 cf=0

mov eax,5
mov ebx,10
cmp eax,ebx ; eax !> ebx
jbe jump ; cf=1 af=1 zf=0

; JNL(不小于则跳 同JGE)/JNG(不大于则跳 同JLE) 跳转标志:(lef !>< right) 有符号
mov eax,10
mov ebx,-5
cmp eax,ebx ; eax !< ebx
jnl jump ; sf=0 cf=1 pf=1 af=1 zf=0

mov eax,-10
mov ebx,5
cmp eax,ebx ; eax !> ebx
jng jump ; sf=1

jump:
xor eax,eax
xor ebx,ebx

invoke ExitProcess,0
main ENDP
END main

BT/BSF/BSR 位测试指令: 首先BT系列命令主要用于对特定寄存器进行测试,清除,设置或求反等操作,它会影响CF标志位,而BSF/BSR命令则是对特定位中的值进行正反向扫描操作,它会影响ZF标志位.

	.386p
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
main PROC
; bt 普通的位测试命令
xor edx,edx
mov dx,10000001b
bt dx,7 ; 把DX第7位送入CF = 1
bt dx,6 ; 把DX第六位送入CF = 0

; bts 位测试并置位
mov dx,10000001b
bts dx,6 ; cf = 0
bts dx,7 ; cf = 1

; btr 位测试并复位.在执行BT同时,把操作数的指定位置为0
mov dx,10000001b
btr dx,7
btr dx,6 ; cf = 0

;btc 位测试并取反.在执行BT同时,把操作数的指定位取反
mov dx,10000001b
btc dx,0 ; cf = 1
btc dx,0 ; cf = 0

; BSF 执行位扫描 由低->高位 | BSR 由高 -> 到低
xor edx,edx
mov dx, 0000111100001100b
bsf cx,dx ; 正向扫描,将扫描到1的位置放入CX
bsr cx,dx ; 反向扫描 zf=0 pf=0

xor ecx,ecx
mov cx,0
mov dx,0
bsf cx,dx
lahf

invoke ExitProcess,0
main ENDP
END main