汇编,局部变量和函数(Win32,NASM)

原文:Assembly, Local Variables and Functions (Win32, NASM)

先看以下这段C++代码:

#include <iostream> 

using namespace std; 

int main(int argc, char * argv[]){ 
    char yourname[512]; 
    char howyoudo[512]; 
    int somenumber; 
    cout<<"Hello, please enter your name: \r\n"; 
    cin>>yourname; 
    cout<<"Hello "<<yourname<<", how are you doing today? \r\n"; 
    cin>>howyoudo; 
    cout<<"Anyway, I've got to go, now. \r\nSee you later! \r\n"; 
    for (somenumber= 0; somenumber < 10; somenumber++) cin.get(); 
}

上述代码段中,我们要关注的部分是函数和局部变量。局部变量是存储在堆栈中的变量。存储在程序数据段或bss节中的变量称为全局变量。
在查看上述C++函数过程中,对于变量需要注意哪些问题?首先需要注意的是在函数内部定义的变量;其次需要注意的是在括号内定义的变量(在函数名称之后),这些变量称为参数。

基本函数

让我们看看基本函数的工作原理:

call my_function 

;; some more code...  

my_function: 
;; .....  
ret

在上面的代码执行过程中,执行到call my_function语句时,EIP被压入(pushed)到堆栈(stack),调用(call)函数my_function。然后,在 RET 指令被执行时,堆栈中前 4 个字节弹出(poped)到 EIP 寄存器中。
换句话说,调用 my_function 代码,然后在执行 RET 指令时,处理器返回到调用函数的代码。

调用约定(Calling Conventions)

首先,调用(call)函数时,所有参数将被逆序压入到堆栈中。
但是,当参数被压入堆栈时堆栈指针(stack pointer)将会被自动更改。因此,在调用的函数时必须考虑将堆栈指针更改回调用前的值。作为编程人员如何来正确更改堆栈指针取决于使用的调用约定(calling convention)。
调用约定是指调用函数(calling functions)的方法。

标准调用约定(Standard Calling Convention)

标准调用(Standard Calling)充分利用了 RET 指令可以指定可选操作数(optional operand)的特点。

RET - Return

返回指令可以带有一个可选的操作数。

格式:

RET n

操作:

POP EIP
ESP= ESP + n

如果提供操作数 n ,则 ESP 寄存器的值将在返回后加上n,以清理堆栈。否则,ESP 在返回后将保持不变。请注意,n 不包括用于指向返回指令的指针的 4 个字节。

因此,如果采用标准调用,可以使用以下方法调用某个函数:

push dword 0 
call my_function 

;; some more code...  

my_function: 
;; .....  
ret 4

除了标准调用约定之外,还有 C 调用约定。

C调用约定

在 C 调用约定中,调用者(caller)必须清理堆栈。当参数数量可变时,此约定很有用。因此,要使用 C 调用约定调用函数,需要使用以下所给方法:

push dword 0 
call my_function 
add esp, 4 

;; some more code...  

my_function: 
;; .....  
ret

现在可以通过上述两种方法调用函数,下边来了解如何访问参数(access arguments)。

访问参数

大多数 Win32 API 函数使用标准调用约定。
在访问参数时,要记住的一件事是除非你真正知道你在做什么,否则切勿使用 POP 指令来获取参数。通常可以使用更为简单的 MOV 语法。以下代码在 EAX 中返回第一个参数:

my_function: 
mov eax, dword [esp+4] 
ret 4

由于 [esp] 中的值包含返回指令指针,因此明智的做法是不要将它误认为是第一个参数。
调用者(caller)将参数压入堆栈后,ESP 将指向参数。但是,当调用者(caller)调用函数时,EIP(大小为 4 字节)也会被压入堆栈,这意味着 ESP 现在指向的实际时返回指令指针,因此必须使用 ESP+4 跳过指向返回指令的指针,从而使得ESP正确地指向第一个参数。此方法对于小函数来说很好,但如果函数越大,则有更好的方法。

设置堆栈帧(Stack Frame)

首先来看一个问题: EBP 寄存器是什么?
以下代码可以帮助回答和理解上述问题(What this EBP register is?)

;; the prologue: 
push ebp     ;; save ebp 
mov ebp, esp     ;; now save esp 
;; note that, at this point, previous ebp = [ [ ebp ] ] 
;; that, obviously, is not allowed, but there is a way to 
;; restore ebp using one or two instructions. 
sub esp, 4 
;; reserve 4 bytes on the stack for local variables. 


;; let's say we call another function and that function does this: 
;; the prologue: 
push ebp ;; save ebp 
mov ebp, esp ;; save esp 
sub esp, 16  ;; reserve 16 bytes on the stack for local variables 

;; some code ...  

;; the epilogue: 
mov esp, ebp  ;; restore esp 
pop ebp         ;; restore ebp 

;; then it's time for our first function to exit 
;; the epilogue: 
mov esp, ebp  ;; restore esp 
pop ebp         ;; restore ebp

上述代码中的序言(prologue)部分是设置新堆栈帧的方法。结语(epilogue)部分是切换回上一个堆栈帧的方法。
当我们将 EBP 压入堆栈时,我们相当于保存了 EBP。然后,我们通过将 ESP 放入 EBP 来保存 ESP。稍后,我们再次从 EBP 获取 ESP,之后我们就可以恢复 EBP 之前的值。这就是 EBP 寄存器的使用目的与方式。

设置堆栈帧(Stack Frame)

另一种设置堆栈帧的方法是使用 ENTERLEAVE 指令。

enter 16, 0
is the same as
push ebp
mov ebp, esp
sub esp, 16
leave
is the same as
mov esp, ebp
pop ebp

但是,此方法不一定比其他方法更快,但不管怎样,这种方法更简单,更突出。那么,如何使用堆栈帧呢?

使用堆栈帧(Stack Frame)

如果我们不知道如何使用堆栈帧,知道如何设置堆栈帧将无济于事。要使用堆栈帧,我们需要通过EBP寄存器使用有效寻址(effective addressing)。当函数被调用(called)时,返回指令指针(长度为4个字节)将压入到堆栈。当我们设置堆栈帧时,EBP寄存器的值(长度为4个字节)也将压入到堆栈。因此,现在我们必须跳过总共 8 个字节,才能到达第一个参数。但这次我们使用EBP 寄存器,而不是 ESP 寄存器,如上一个示例所示。
假设 Win32 API 函数中,每个参数长度均为 4 个字节。

five_argument_function: 
enter 0, 0 

;; Get the first argument. 
mov eax, dword [ebp+8] 

;; Get the second argument. 
mov eax, dword [ebp+12] 
;; Note that the address of the second argument is 
;; equal to the address of the first argument plus the 
;; size of the first argument. 
;; The address of the first argument is EBP+8 and the 
;; size of each argument, in this case, is 4, so 
;; EBP+8 + 4 is the same as EBP+12 

;; Get the third argument. 
mov eax, dword [ebp+16] 

;; Get the fourth argument. 
mov eax, dword [ebp+20] 

;; Get the fifth argument. 
mov eax, dword [ebp+24] 

leave 
ret 20

在上述示例代码中,我们看到第 n 个参数的地址等于 EBP+8 = ( n - 1) * 4)。此外,由于共有 5 个参数,每个参数为 4 个字节,因此我们使用 RET 指令时通过可选的参数 20 ,释放 5 * 4 = 20 个字节以平衡堆栈。
我们已了解如何访问函数参数,下边接着讨论如何使用局部变量。

局部变量

要为本地变量在堆栈上保留空间,我们需要以某种方式减去要从 ESP 保留的字节数。方法之一是使用SUB指令,但也有其他方法,例如定义全局变量。
下边让我们看看以下示例代码:

my_function: 
push ebp 
mov ebp, esp 
push dword 65 

;; some more code...  

mov esp, ebp 
pop ebp 
ret 0

在上述代码中,我们设置了一个新的堆栈帧,在堆栈上保留4个字节,并设置我们的本地变量值为65。如果我们仔细考虑该代码,就会发现它与下边所给示例代码相同:

my_function: 
push ebp 
mov ebp, esp 
sub esp, 4 

mov dword [ebp-4], 65 

;; some more code...  

mov esp, ebp 
pop ebp 
ret 0

如果我们保留 n 个字节的堆栈空间,我们可以认为通过内存地址范围 EBP-1EBP-n 所访问的是局部变量。至于我们如何管理堆栈空间是我们的选择,这正是汇编语言的强项。

Win32 结构(Structures)

如前所述,如何使用本地变量,甚至全局变量,是我们的选择。在汇编语言中,获得正确的结果才是真正重要的(getting the correct results is really the matters)。
但我们创建新窗口时,需要一个窗口类。要创建新的窗口类,需要实现一个WNDCLASSEX 结构。当我们实现 WNDCLASSEX 结构时,首先需要知道将其存放在哪里,可选方案之一就是将其保存为局部变量。
WNDCLASSEX 结构的大小为 48 字节,因此我们必须在堆栈上保留至少 48 个字节用于局部变量。
如果 WNDCLASSEX 结构在堆栈中的存放的起始地址为 [ebp-48],则结构的第一个条目(entry)为 [ebp-48],第二个条目为 [ebp-44],第三个 [ebp-40],其他以此类推。

LEA指令 - 加载有效地址

LEA 指令在处理局部变量时非常有用。到目前为止,要获得 [ebp-4] 的地址,我们必须这样做:

mov eax, ebp 
sub eax, 4

但是使用 LEA 指令,我们可以使用有效的地址,就像我们正在访问内存一样:

lea eax, [ebp-4]

将上述一切组合到一个程序中

程序预计实现的功能

  1. Call main
  2. Exit, returning 0

函数main实现的功能

  1. 局部变量 lvar1 = my_function
  2. 在消息框上显示文本msg1
  3. 调用 [局部变量 lvar1],以 msg2 作为参数。
  4. 调用c_function, 以 msg3 作为参数。
  5. 返回。

my_function实现的功能

  1. 将第一个参数显示为文本的在消息框中以文本形式显示第1个参数。
  2. 返回。

c_function实现的功能

  1. 在堆栈上保留 4 个字节,并设置局部变量 lvar1 为第一个参数。
  2. 将本地变量 1 显示为文本的在消息框中以文本形式显示局部变量 lvar1
  3. 返回。

其中,mainmy_function两个函数采用标准调用函数约定,c_function函数采用C 调用约定。

实现上述功能的示例代码:

;; We define the externs. 
extern MessageBoxA                    ;; MessageBox is one of those functions that has a unicode version, so we have to use the A suffix. 
extern ExitProcess 

;; Then we have the symbol import table. 
import MessageBoxA user32.dll                ;; MessageBox is a function defined in user32.dll 
import ExitProcess kernel32.dll              ;; ExitProcess is part of kernel32.dll 

;; This is the code section; use 32-bit code. 
section .text use32 
;; This is where the program entry point is. 
..start: 

;; main(); 
call main 

;; ExitProcess(0); 
push dword 0 
call [ExitProcess] 

;; This is our main() function; though it doesn't take any arguments. 
;; We could modify the code to scan the command line and obtain the 
;; argc and argv values, but what we have is fine for now. 
main: 
    enter 4, 0 
    
    ;; [ebp-4]= my_function; 
    mov dword [ebp-4], my_function 
    
    ;; [ebp-4] is a local variable. 
    ;; At the moment, it stores the address of 
    ;; our my_function() function. 
    
    ;; MessageBoxA(0, msg1, the_title, 0); 
    push dword 0 
    push dword the_title 
    push dword msg1 
    push dword 0 
    call [MessageBoxA] 
    
    ;; [ebp-4](msg2); 
    push dword msg2 
    call [ebp-4] 
    
    ;; c_function(msg3); 
    push dword msg3 
    call c_function 
    add esp, 4 
    
    leave 
ret 0 

;; This is a standard calling convention function. 
my_function: 
    push ebp 
    mov ebp, esp 
    
    ;; [ebp+8] is the first argument. 
    
    ;; MessageBox(0, [ebp+8], the_title, 0); 
    push dword 0 
    push dword the_title 
    push dword [ebp+8] 
    push dword 0 
    call [MessageBoxA] 
    
    mov esp, ebp 
    pop ebp 
ret 4 

;; This is a C calling convention function. 
c_function: 
    enter 0, 0 
    push dword [ebp+8] 
    
    ;; [ebp-4] is now equal to the first argument, 
    ;; due to the PUSH DWORD [EBP+8] instruction, 
    ;; which reserves 4 bytes on the stack and, 
    ;; at the same time, sets those 4 bytes to 
    ;; the value of the first argument. 
    
    push dword [ebp-4] 
    call my_function 
    
    leave 
ret 

;; This goes into the data section. 
section .data 
;; We define the data global variables. 
the_title                                 db "Local Variables Test", 0 
msg1                                      db "Hello World! ", 13, 10, 0 
msg2                                      db "Oh, you're still there? ", 13, 10, 13, 10, "Well then, hello again.", 0 
msg3                                      db "Come on, don't be silly. ", 13, 10, "Aren't you ever going to leave? ", 13, 10, 13, 10 
db "Oh... wait... I'm the one who's not leaving. My bad. ", 13, 10, 0 

;; The following goes into the bss section. 
section .bss 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值