5.10 汇编语言:汇编过程与结构
过程的实现离不开堆栈的应用,堆栈是一种后进先出(LIFO)
的数据结构,最后压入栈的值总是最先被弹出,而新数值在执行压栈时总是被压入到栈的最顶端,栈主要功能是暂时存放数据和地址,通常用来保护断点和现场。
栈是由CPU
管理的线性内存数组,它使用两个寄存器(SS和ESP)
来保存栈的状态,SS寄存器存放段选择符,而ESP寄存器的值通常是指向特定位置的一个32位偏移值,我们很少需要直接操作ESP寄存器,相反的ESP寄存器总是由CALL,RET,PUSH,POP
等这类指令间接性的修改。
CPU提供了两个特殊的寄存器用于标识位于系统栈顶端的栈帧。
- ESP 栈指针寄存器:栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
- EBP 基址指针寄存器:基址指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
在通常情况下ESP是可变的,随着栈的生成而逐渐变小,而EBP寄存器是固定的,只有当函数的调用后,发生入栈操作而改变。
- 执行PUSH压栈时,堆栈指针自动减4,再将压栈的值复制到堆栈指针所指向的内存地址。
- 执行POP出栈时,从栈顶移走一个值并将其复制给内存或寄存器,然后再将堆栈指针自动加4。
- 执行CALL调用时,CPU会用堆栈保存当前被调用过程的返回地址,直到遇到RET指令再将其弹出。
10.1 PUSH/POP
PUSH和POP是汇编语言中用于堆栈操作的指令,它们通常用于保存和恢复寄存器的值,参数传递和函数调用等。
PUSH指令用于将操作数压入堆栈中,它执行的操作包括将操作数复制到堆栈的栈顶,并将堆栈指针(ESP)减去相应的字节数。指令格式如下:
PUSH operand
其中,operand可以是8位,16位或32位的寄存器,立即数,以及内存中的某个值。例如,要将寄存器EAX的值压入堆栈中,可以使用以下指令:
PUSH EAX
从汇编代码的角度来看,PUSH指令将操作数存储到堆栈中,它实际上是一个入栈操作。
POP指令用于将堆栈中栈顶的值弹出到指定的目的操作数中,它执行的操作包括将堆栈顶部的值移动到指定的操作数,并将堆栈指针增加相应的字节数。指令格式如下:
POP operand
其中,operand可以是8位,16位或32位的寄存器,立即数,以及内存中的某个位置。例如,要将从堆栈中弹出的值存储到BX寄存器中,可以使用以下指令:
POP EBX
从汇编代码的角度来看,POP指令将从堆栈中取出一个值,并将其存储到目的操作数中,它是一个出栈操作。
在函数调用时,PUSH指令被用于向堆栈中推送函数的参数,这些参数可以是寄存器、立即数或者内存中的某个值。在函数返回之前,POP指令被用于将堆栈顶部的值弹出,并将其存储到寄存器或者内存中。
读者需要特别注意,在使用PUSH
和POP
指令时需要保证堆栈的平衡,也就是说,每个PUSH
指令必须有对应的POP
指令,否则堆栈会失去平衡,最终导致程序出现错误。
在读者了解了这两条指令时则可以执行一些特殊的操作,如下代码我们以数组入栈与出栈为例,执行PUSH
指令时,首先减小ESP
的值,然后把源操作数复制到堆栈上,执行POP
指令则是先将数据弹出到目的操作数中,然后再执行ESP
值增加4,并以此分别将数组中的元素压入栈,最终再通过POP将元素反弹出来。
.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 1,2,3,4,5,6,7,8,9,10
szFmt BYTE '%d ',0dh,0ah,0
.code
main PROC
; 使用Push指令将数组正向入栈
mov eax,0
mov ecx,10
S1:
push dword ptr ds:[Array + eax * 4]
inc eax
loop S1
; 使用pop指令将数组反向弹出
mov ecx,10
S2:
push ecx ; 保护ecx
pop ebx ; 将Array数组元素弹出到ebx
invoke crt_printf,addr szFmt,ebx
pop ecx ; 弹出ecx
loop S2
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
MyString BYTE "hello lyshark",0
NameSize DWORD ($ - MyString) - 1
szFmt BYTE '%s',0dh,0ah,0
.code
main PROC
; 正向压入字符串
mov ecx,dword ptr ds:[NameSize]
mov esi,0
S1: movzx eax,byte ptr ds:[MyString + esi]
push eax
inc esi
loop S1
; 反向弹出字符串
mov ecx,dword ptr ds:[NameSize]
mov esi,0
S2: pop eax
mov byte ptr ds:[MyString + esi],al
inc esi
loop S2
invoke crt_printf,addr szFmt,addr MyString
int 3
main ENDP
END main
10.2 PROC/ENDP
PROC/ENDP 伪指令是用于定义过程(函数)的伪指令,这两个伪指令可分别定义过程的开始和结束位置。此处读者需要注意,这两条伪指令并非是汇编语言中所兼容的,而是MASM
编译器为我们提供的一个宏,是MASM
的一部分,它允许程序员使用汇编语言定义过程(函数)可以像标准汇编指令一样使用。
对于不使用宏定义来创建函数时我们通常会自己管理函数栈参数,而有了宏定义这些功能都可交给编译器去管理,下面的一个案例中,我们通过使用过程创建ArraySum
函数,实现对整数数组求和操作,函数默认将返回值存储在EAX
中,并打印输出求和后的参数。
.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 1,2,3,4,5,6,7,8,9,10
Sum DWORD ?
szFmt BYTE '%d',0dh,0ah,0
.code
; 数组求和过程
ArraySum PROC
push esi ; 保存ESI,ECX
push ecx
xor eax,eax
S1: add eax,dword ptr ds:[esi] ; 取值并相加
add esi,4 ; 递增数组指针
loop S1
pop ecx ; 恢复ESI,ECX
pop esi
ret
ArraySum endp
main PROC
lea esi,dword ptr ds:[MyArray] ; 取出数组基址
mov ecx,lengthof MyArray ; 取出元素数目
call ArraySum ; 调用方法
mov dword ptr ds:[Sum],eax ; 得到结果
invoke crt_printf,addr szFmt,Sum
int 3
main ENDP
END main
接着我们来实现一个具有获取随机数功能的案例,在C语言中如果需要获得一个随机数一般会调用Seed
函数,如果读者逆向分析过这个函数的实现原理,那么读者应该能理解,在调用取随机数之前会生成一个随机数种子,这个随机数种子的生成则依赖于0x343FDh
这个特殊的常量地址,当我们每次访问该地址都会产出一个随机的数据,当得到该数据后,我们再通过除法运算取出溢出数据作为随机数使用实现了该功能。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
seed DWORD 1
szFmt BYTE '随机数: %d',0dh,0ah,0
.code
; 生成 0 - FFFFFFFFh 的随机种子
Random32 PROC
push edx
mov eax, 343FDh
imul seed
add eax, 269EC3h
mov seed, eax
ror eax,8
pop edx
ret
Random32 endp
; 生成随机数
RandomRange PROC
push ebx
push edx
mov ebx,eax
call Random32
mov edx,0
div ebx
mov eax,edx
pop edx
pop ebx
ret
RandomRange endp
main PROC
; 调用后取出随机数
call RandomRange
invoke crt_printf,addr szFmt,eax
int 3
main ENDP
END main
10.3 局部参数传递
在汇编语言中,可以使用堆栈来传递函数参数和创建局部变量。当程序执行到函数调用语句时,需要将函数参数传递给被调用函数。为了实现参数传递,程序会将参数压入栈中,然后调用被调用函数。被调用函数从栈中弹出参数并执行,然后将返回值存储在寄存器中,最后通过跳转返回到调用函数。
局部变量也可以通过在栈中分配内存来创建。在函数开始时,可以使用push指令将局部变量压入栈中。在函数结束时,可以使用pop指令将变量从栈中弹出。由于栈是后进先出的数据结构,局部变量的创建可以很方便地通过在栈上压入一些数据来实现。
局部变量是在程序运行时由系统动态的在栈上开辟的,在内存中通常在基址指针(EBP)
之下,尽管在汇编时不能给定默认值,但可以在运行时初始化,如下一段C语言伪代码:
void MySub()
{
int var1 = 10;
int var2 = 20;
}
上述的代码经过C编译后,会变成如下汇编指令,其中EBP-4
必须是4的倍数,因为默认就是4字节存储,如果去掉了mov esp,ebp
,那么当执行pop ebp
时将会得到EBP
等于10,执行RET
指令会导致控制转移到内存地址10处执行,从而程序会崩溃。
MySub PROC
push ebp ; 将EBP存储在栈中
mov ebp,esp ; 堆栈框架的基址
sub esp,8 ; 创建局部变量空间(分配2个局部变量)
mov DWORD PTR [ebp-8],10 ; var1 = 10
mov DWORD PTR [ebp-4],20 ; var2 = 20
mov esp,ebp ; 从堆栈上删除局部变量
pop ebp ; 恢复EBP指针
ret 8 ; 返回,清理堆栈
MySub ENDP
为了使上述代码片段更易于理解,可以在上述的代码的基础上给每个变量的引用地址都定义一个符号,并在代码中使用这些符号,如下代码所示,代码中定义了一个名为MySub
的过程,该过程将两个局部变量分别设置为10
和20
。
在该过程中,首先使用push ebp
指令将旧的基址指针压入栈中,并将ESP
寄存器的值存储到ebp
中。这个旧的基址指针将在函数执行完毕后被恢复。然后,我们使用sub esp,8
指令将8
字节的空间分配给两个局部变量。在堆栈上分配的空间可以通过var1_local
和var2_local
符号来访问。在这里,我们定义了两个符号,将它们与ebp
寄存器进行偏移以访问这些局部变量。var1_local
的地址为[ebp-8]
,var2_local
的地址为[ebp-4]
。然后,我们使用mov
指令将10
和 20
分别存储到这些局部变量中。最后,我们将ESP
寄存器的值存储回ebp
中,并使用pop ebp
指令将旧的基址指针弹出堆栈。现在,栈顶指针(ESP)下移恢复上面分配的8个字节的空间,最后通过ret 8
返回到调用函数。
在使用堆栈传参和创建局部变量时,需要谨慎考虑栈指针的位置,并确保遵守调用约定以确保正确地传递参数和返回值。
var1_local EQU DWORD PTR [ebp-8] ; 添加符号1
var2_local EQU DWORD PTR [ebp-4] ; 添加符号2
MySub PROC
push ebp
mov ebp,esp
sub esp,8
mov var1_local,10
mov var2_local,20
mov esp,ebp
pop ebp
ret 8
MySub ENDP
接着我们来实现一个具有功能的案例,首先为了能更好的让读者理解我们先使用C语言方式实现MakeArray()
函数,该函数的内部是动态生成的一个MyString
数组,并通过循环填充为星号字符串,最后使用POP
弹出,并输出结果,观察后尝试用汇编实现。
void makeArray()
{
char MyString[30];
for(int i=0;i<30;i++)
{
myString[i] = "*";
}
}
call makeArray()
上述C语言代码如果翻译为汇编格式则如下所示,代码使用汇编语言实现makeArray
的程序,该程序开辟了一个长度为30
的数组,将其中的元素填充为*
,然后弹出两个元素,并将它们输出到控制台。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
szFmt BYTE '出栈数据: %x ',0dh,0ah,0
.code
makeArray PROC
push ebp
mov ebp,esp
; 开辟局部数组
sub esp,32 ; MyString基地址位于 [ebp - 30]
lea esi,[ebp - 30] ; 加载MyString的地址
; 填充数据
mov ecx,30 ; 循环计数
S1: mov byte ptr ds:[esi],'*' ; 填充为*
inc esi ; 每次递增一个字节
loop S1
; 弹出2个元素并输出,出栈数据
pop eax
invoke crt_printf,addr szFmt,eax
pop eax
invoke crt_printf,addr szFmt,eax
; 以下平栈,由于我们手动弹出了2个数据
; 则平栈 32 - (2 * 4) = 24
add esp,24 ; 平栈
mov esp,ebp
pop ebp ; 恢复EBP
ret
makeArray endp
main PROC
call makeArray
invoke ExitProcess,0
main ENDP
END main
在该程序的开始部分,我们首先通过push ebp
和mov ebp,esp
指令保存旧的基址指针并将当前栈顶指针(ESP)
存储到ebp
中。然后,我们使用sub esp, 32
指令开辟一个长度为30
的数组MyString
。我们将MyString
数组的基地址存储在[ebp - 30]
的位置。使用lea esi, [ebp - 30]
指令将MyString
的基地址加载到esi
寄存器中。该指令偏移ebp-30
是因为ebp-4
是MakeArray
函数的第一个参数的位置,因此需要增加四个字节。我们利用MOV byte ptr ds:[esi],'*'
指令将MyString
中的所有元素填充为*
。
然后,使用pop eax
和invoke crt_printf, addr szFmt, eax
指令两次弹出两个元素,并使用crt_printf
函数输出这些元素。该函数在msvcrt.dll
库中实现,用于将格式化的信息输出到控制台。在输出数据之后,我们通过add esp,24
和mov esp,ebp
指令将堆栈平衡,恢复旧的基址指针ebp
,然后从堆栈中弹出ebp
,并通过ret
指令返回到调用程序。
接着我们继续来对比一下堆栈中参数传递的异同点,平栈的方式一般可分为调用者平栈和被调用者平栈,在使用堆栈传参时,需要平衡栈以恢复之前的堆栈指针位置。
-
当平栈由被调用者完成时,被调用函数使用
ret
指令将控制权返回到调用函数,并从堆栈中弹出返回地址。此时,被调用函数需要将之前分配的局部变量从堆栈中弹出,以便调用函数能够恢复堆栈指针的位置。因此,被调用函数必须知道其在堆栈上分配的内存大小,并将该大小与其ret
指令中的参数相匹配,以便调用函数可以正确恢复堆栈指针位置。 -
当平栈由调用者完成时,调用函数需要在调用子函数之前平衡堆栈。因此,调用函数需要知道子函数在堆栈上分配的内存大小,并在调用子函数之前向堆栈提交额外的空间。调用函数可以使用
add esp, N
指令来恢复堆栈指针的位置,其中 N 是被调用函数在堆栈上分配的内存大小。然后,调用函数调用被调用函数,该函数将返回并将堆栈指针恢复到调用函数之前的位置。
如下这段汇编代码中笔者分别实现了两种调用方式,其中MyProcA
函数是一种被调用者平栈,由于调用者并没有堆栈修正所以需要在函数内部通过使用ret 12
的方式平栈,之所以是12是因为我们使用了三个局部变量,而第二个MyProcB
函数则是调用者平栈,该方式在函数内部并没有返回任何参数,所以在调用函数结束后需要通过add esp,4
的方式对堆栈进行修正。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
szFmt BYTE '数据: %d ',0dh,0ah,0
.code
; 第一种方式:被调用者平栈
MyProcA PROC
push ebp
mov ebp,esp
xor eax,eax
mov eax,dword ptr ss:[ebp + 16] ; 获取第一个参数
mov ebx,dword ptr ss:[ebp + 12] ; 获取第二个参数
mov ecx,dword ptr ss:[ebp + 8] ; 获取第三个参数
add eax,ebx
add eax,ebx
add eax,ecx
mov esp,ebp
pop ebp
ret 12 ; 此处ret12可平栈,也可使用 add ebp,12
MyProcA endp
; 第二种方式:调用者平栈
MyProcB PROC
push ebp
mov ebp,esp
mov eax,dword ptr ss:[ebp + 8]
add eax,10
mov esp,ebp
pop ebp
ret
MyProcB endp
main PROC
; 第一种被调用者MyProcA平栈 3*4 = 12
push 1
push 2
push 3
call MyProcA
invoke crt_printf,addr szFmt,eax
; 第二种方式:调用者平栈
push 10
call MyProcB
add esp,4
invoke crt_printf,addr szFmt,eax
int 3
main ENDP
END main
当然了如果读者认为自己维护堆栈很繁琐,则此时可以直接使用MASM
汇编器提供的PROC
定义过程,使用该伪指令汇编器会自行计算所需要使用的变量数量并自行在结尾处添加对应的平栈语句,这段代码实现起来将变得非常容易理解。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
.data
szFmt BYTE '计算参数: %d ',0dh,0ah,0
.code
my_proc PROC x:DWORD,y:DWORD,z:DWORD ; 定义过程局部参数
LOCAL @sum:DWORD ; 定义局部变量存放总和
mov eax,dword ptr ds:[x]
mov ebx,dword ptr ds:[y] ; 分别获取到局部参数
mov ecx,dword ptr ds:[z]
add eax,ebx
add eax,ecx ; 相加后放入eax
mov @sum,eax
ret
my_proc endp
main PROC
LOCAL @ret_sum:DWORD
push 10
push 20
push 30 ; 传递参数
call my_proc
mov @ret_sum,eax ; 获取结果并打印
invoke crt_printf,addr szFmt,@ret_sum
int 3
main ENDP
END main
这里笔者还需要扩展一个伪指令LOCAL
,LOCAL是一种汇编语言中的伪指令,用于定义存储在堆栈上的局部变量。使用LOCAL
指令定义的局部变量只在函数执行时存在,当函数返回后,该变量将被删除。根据使用LOCAL
指令时指定的内存空间大小,汇编器将为每个变量保留足够的空间。
例如,下面是一个使用LOCAL定义局部变量的示例:
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
main PROC
; 定义局部变量,自动压栈/平栈
LOCAL var_byte:BYTE,var_word:WORD,var_dword:DWORD
LOCAL var_array[3]:DWORD
; 填充局部变量
mov byte ptr ds:[var_byte],1
mov word ptr ds:[var_word],2
mov dword ptr ds:[var_dword],3
; 填充数组方式1
lea esi,dword ptr ds:[var_array]
mov dword ptr ds:[esi],10
mov dword ptr ds:[esi + 4],20
mov dword ptr ds:[esi + 8],30
; 填充数组方式2
mov var_array[0],100
mov var_array[1],200
mov var_array[2],300
invoke ExitProcess,0
main ENDP
END main
在上述示例代码中,main
过程使用LOCAL
指令定义了几个局部变量,包括一个字节类型的变量var_byte
、一个字类型的变量var_word
、一个双字类型的变量var_dword
和一个包含三个双字元素的数组var_array
。
在代码中,我们使用mov
指令填充这些变量的值。对于字节类型、字类型和双字类型的变量,使用mov byte ptr ds:[var_byte], 1
、mov word ptr ds:[var_word], 2
和mov dword ptr ds:[var_dword], 3
指令将相应的常数值存储到变量中。在填充数组时,分别使用了两种不同的方式。一种方式是使用lea
指令将数组的地址加载到esi
寄存器中,然后使用mov dword ptr ds:[esi],10
等指令将相应的常数值存储到数组中。另一种方式是直接访问数组元素,如mov var_array[0], 100
等指令。需要注意,由于数组元素在内存中是连续存储的,因此可以使用[]
操作符访问数组元素。
在汇编中使用LOCAL
伪指令来实现自动计算局部变量空间,以及最后的平栈操作,将会极大的提高开发效率。
10.4 USES/ENTER
USES是汇编语言中的伪指令,用于保存一组寄存器的状态,以便函数调用过程中可以使用这些寄存器。使用USES时,程序可以保存一组需要保护的寄存器,汇编器将在程序入口处自动向堆栈压入这些寄存器的值。读者需注意,我们可以在需要保存寄存器的程序段中使用USES来保护寄存器,但不应在整个程序中重复使用寄存器。
ENTER也是一种伪指令,用于创建函数调用过程中的堆栈帧。使用ENTER时,程序可以定义一个名为ENTER的指定大小的堆栈帧。该指令会将新的基准指针ebp 压入堆栈同时将当前的基准指针ebp存储到另一个寄存器ebx中,然后将堆栈指针esp减去指定大小的值,获取新的基地址,并将新的基地址存储到ebp 中。之后,程序可以在此帧上创建和访问局部变量,并使用LEAVE指令将堆栈帧删除,将ebp恢复为旧的值,同时将堆栈指针平衡。
在使用USES和ENTER指令时,需要了解这些指令在具体的平台上的支持情况,以及它们适用的调用约定。通常情况下,在函数开头,我们将使用ENTER创建堆栈帧,然后使用USES指定需要保护的寄存器。在函数末尾,我们使用LEAVE删除堆栈帧。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.code
; USES 自动压入 eax,ebx,ecx,edx
my_proc PROC USES eax ebx ecx edx x:DWORD,y:DWORD
enter 8,0 ; 自动保留8字节堆栈空间
add eax,ebx
leave
my_proc endp
main PROC
mov eax,10
mov ebx,20
call my_proc
int 3
main ENDP
END main
10.5 STRUCT/UNION
STRUCT和UNION是汇编语言中的数据类型,STRUCT是一种复合数据类型,它将多个不同类型的变量按顺序放置在一起,并使用单个名称来引用集合。使用STRUCT时,我们可以将不同类型的变量组合成一个结构体并定义其属性,如结构体中包含的成员变量的数据类型、名称和位置。
例如,下面是一个使用STRUCT定义自定义类型的示例:
; 定义一个名为 MyStruct 的结构体,包含两个成员变量。
MyStruct STRUCT
Var1 DWORD ?
Var2 WORD ?
MyStruct ENDS
在上述示例代码中,我们使用STRUCT
定义了一个名为MyStruct
的结构体,其中包含两个成员变量Var1
和Var2
。其中,Var1
是DWORD
类型的数据类型,以问号?
形式指定了其默认值,Var2
是WORD
类型的数据类型。
另一个数据类型是UNION
,它也是一种复合数据类型,用于将多个不同类型的变量叠加在同一内存位置上。使用UNION
时,程序内存中的数据将只占用所有成员变量中最大的数据类型变量的大小。与结构体不同,联合中的所有成员变量共享相同的内存位置。我们可以使用一种成员变量来引用内存位置,但在任何时候仅能有一种成员变量存储在该内存位置中。
例如,下面是一个使用UNION定义自定义类型的示例:
; 定义一个名为 MyUnion 的联合,包含两个成员变量。
MyUnion UNION
Var1 DWORD ?
Var2 WORD ?
MyUnion ENDS
在上述示例代码中,我们使用UNION
定义了一个名为MyUnion
的联合,其中包含两个不同类型的成员变量Var1
和Var2
,将它们相对应地置于联合的同一内存位置上。
读者在使用STRUCT
和UNION
时,需要根据内存分布和变量类型来正确访问成员变量的值。在汇编语言中,结构体和联合主要用于定义自定义数据类型、通信协议和系统数据结构等,如下一段代码则是汇编语言中实现结构体赋值与取值的总结。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
; 定义坐标结构
MyPoint Struct
pos_x DWORD ?
pos_y DWORD ?
pos_z DWORD ?
MyPoint ends
; 定义人物结构
MyPerson Struct
Fname db 20 dup(0)
fAge db 100
fSex db 20
MyPerson ends
.data
; 声明结构: 使用 <>,{}符号均可
PtrA MyPoint <10,20,30>
PtrB MyPoint {100,200,300}
; 声明结构: 使用MyPerson声明结构
UserA MyPerson <'lyshark',24,1>
.code
main PROC
; 获取结构中的数据
lea esi,dword ptr ds:[PtrA]
mov eax,(MyPoint ptr ds:[esi]).pos_x
mov ebx,(MyPoint ptr ds:[esi]).pos_y
mov ecx,(MyPoint ptr ds:[esi]).pos_z
; 向结构中写入数据
lea esi,dword ptr ds:[PtrB]
mov (MyPoint ptr ds:[esi]).pos_x,10
mov (MyPoint ptr ds:[esi]).pos_y,20
mov (MyPoint ptr ds:[esi]).pos_z,30
; 直接获取结构中的数据
mov eax,dword ptr ds:[UserA.Fname]
mov ebx,dword ptr ds:[UserA.fAge]
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
; 定义坐标结构
MyPoint Struct
pos_x DWORD ?
pos_y DWORD ?
pos_z DWORD ?
MyPoint ends
; 定义循环结构
MyCount Struct
count_x DWORD ?
count_y DWORD ?
MyCount ends
.data
; 声明结构: 使用 <>,{}符号均可
PtrA MyPoint <10,20,30>,<40,50,60>,<70,80,90>,<100,110,120>
Count MyCount <0,0>
szFmt BYTE '结构数据: %d',0dh,0ah,0
.code
main PROC
; 获取结构中的数据
lea esi,dword ptr ds:[PtrA]
mov eax,(MyPoint ptr ds:[esi]).pos_x ; 获取第一个结构X
mov eax,(MyPoint ptr ds:[esi + 12]).pos_x ; 获取第二个结构X
; while 循环输出结构的每个首元素元素
mov (MyCount ptr ds:[Count]).count_x,0
S1: cmp (MyCount ptr ds:[Count]).count_x,48 ; 12 * 4 = 48
jge lop_end
mov ecx,(MyCount ptr ds:[Count]).count_x
mov eax,dword ptr ds:[PtrA + ecx] ; 寻找首元素
invoke crt_printf,addr szFmt,eax
mov eax,(MyCount ptr ds:[Count]).count_x
add eax,12 ; 每次递增12
mov (MyCount ptr ds:[Count]).count_x,eax
jmp S1
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
; 定义坐标结构
MyPoint Struct
pos_x DWORD ?
pos_y DWORD ?
pos_z DWORD ?
MyPoint ends
; 定义循环结构
MyCount Struct
count_x DWORD ?
count_y DWORD ?
MyCount ends
.data
; 声明结构: 使用 <>,{}符号均可
PtrA MyPoint <10,20,30>,<40,50,60>,<70,80,90>,<100,110,120>
Count MyCount <0,0>
szFmt BYTE '结构数据: %d',0dh,0ah,0
.code
main PROC
; 获取结构中的数据
lea esi,dword ptr ds:[PtrA]
mov eax,(MyPoint ptr ds:[esi]).pos_x ; 获取第一个结构X
mov eax,(MyPoint ptr ds:[esi + 12]).pos_x ; 获取第二个结构X
; while 循环输出结构的每个首元素元素
mov (MyCount ptr ds:[Count]).count_x,0
S1: cmp (MyCount ptr ds:[Count]).count_x,48 ; 12 * 4 = 48
jge lop_end
mov (MyCount ptr ds:[Count]).count_y,0
S3: cmp (MyCount ptr ds:[Count]).count_y,12 ; 3 * 4 = 12
jge S2
mov eax,(MyCount ptr ds:[Count]).count_x
add eax,(MyCount ptr ds:[Count]).count_y ; 相加得到比例因子
mov eax,dword ptr ds:[PtrA + eax] ; 使用相对变址寻址
invoke crt_printf,addr szFmt,eax
mov eax,(MyCount ptr ds:[Count]).count_y
add eax,4 ; 每次递增4
mov (MyCount ptr ds:[Count]).count_y,eax
jmp S3
S2: mov eax,(MyCount ptr ds:[Count]).count_x
add eax,12 ; 每次递增12
mov (MyCount ptr ds:[Count]).count_x,eax
jmp S1
lop_end:
int 3
main ENDP
END main
结构体同样支持内嵌的方式,如下Rect
指针中内嵌两个MyPoint
分别指向左子域和右子域,这里顺便定义一个MyUnion
联合体把,其使用规范与结构体完全一致,只不过联合体只能存储一个数据.
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
; 定义坐标结构
MyPoint Struct
pos_x DWORD ?
pos_y DWORD ?
pos_z DWORD ?
MyPoint ends
; 定义左右结构
Rect Struct
Left MyPoint <>
Right MyPoint <>
Rect ends
; 定义联合体
MyUnion Union
my_dword DWORD ?
my_word WORD ?
my_byte BYTE ?
MyUnion ends
.data
PointA Rect <>
PointB Rect {<10,20,30>,<100,200,300>}
test_union MyUnion {1122h}
szFmt BYTE '结构数据: %d',0dh,0ah,0
.code
main PROC
; 嵌套结构的赋值
mov dword ptr ds:[PointA.Left.pos_x],100
mov dword ptr ds:[PointA.Left.pos_y],200
mov dword ptr ds:[PointA.Right.pos_x],100
mov dword ptr ds:[PointA.Right.pos_y],200
; 通过地址定位
lea esi,dword ptr ds:[PointB]
mov eax,dword ptr ds:[PointB] ; 定位第一个MyPoint
mov eax,dword ptr ds:[PointB + 12] ; 定位第二个内嵌MyPoint
; 联合体的使用
mov eax,dword ptr ds:[test_union.my_dword]
mov ax,word ptr ds:[test_union.my_word]
mov al,byte ptr ds:[test_union.my_byte]
main ENDP
END main
当然有了结构体这一成员的加入,我们同样可以在汇编层面实现链表的定义与输出,如下代码所示,首先定义一个ListNode
用于存储链表结构的数据域与指针域,接着使用TotalNodeCount
定义链表节点数量,最后使用REPEAT
伪指令开辟ListNode
对象的多个实例,其中的NodeData
域包含一个1-15
的数据,后面的($ + Counter * sizeof ListNode)
则是指向下一个链表的头指针,通过不断遍历则可输出整个链表。
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include msvcrt.inc
includelib msvcrt.lib
ListNode Struct
NodeData DWORD ?
NextPtr DWORD ?
ListNode ends
TotalNodeCount = 15
Counter = 0
.data
LinkList LABEL PTR ListNode
REPEAT TotalNodeCount
Counter = Counter + 1
ListNode <Counter,($ + Counter * sizeof ListNode)>
ENDM
ListNode<0,0> ; 标志着结构链表的结束
szFmt BYTE '结构地址: %x 结构数据: %d',0dh,0ah,0
.code
main PROC
mov esi,offset LinkList
; 判断下一个节点是否为<0,0>
L1: mov eax,(ListNode PTR [esi]).NextPtr
cmp eax,0
je lop_end
; 显示节点数据
mov eax,(ListNode PTR [esi]).NodeData
invoke crt_printf,addr szFmt,esi,eax
; 获取到下一个节点的指针
mov esi,(ListNode PTR [esi]).NextPtr
jmp L1
lop_end:
int 3
main ENDP
END main
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!