目录
一、汇编的第一个“helloworld”
.386 ; 指定本工程应用的指令集为 80386
.model flat, stdcall ; 模式定义为平坦内存模式,调用模式为 stdcall
option casemap:none ; 选项设定为对大小写敏感
include windows.inc ; 包含 Windows 头文件
include user32.inc ; 包含 user32 头文件
include kernel32.inc ; 包含 kernel32 头文件
includelib user32.lib ; 包含 user32 库文件
includelib kernel32.lib ; 包含 kernel32 库文件
.data ; 数据段
szCaption db 'Win32汇编', 0 ; 定义消息框标题字符串,以 0 结尾
szText db 'Hello World!', 0 ; 定义消息框内容字符串,以 0 结尾
.code ; 代码段
start:
invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK ; 调用 MessageBox 函数显示消息框
invoke ExitProcess, NULL ; 调用 ExitProcess 函数退出进程
end start
功能说明(代码注释用;)
-
invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK
-
调用
MessageBox
函数显示一个消息框。 -
NULL
表示消息框没有所有者窗口。 -
offset szText
和offset szCaption
分别是消息框的内容和标题。 -
MB_OK
表示消息框只有一个 "OK" 按钮。
-
-
invoke ExitProcess, NULL
-
调用
ExitProcess
函数退出当前进程。 -
NULL
表示没有返回值。
-
这段代码的主要功能是显示一个带有 "Hello World!" 内容和 "Win32汇编" 标题的消息框,并在用户点击 "OK" 按钮后退出程序。
二、汇编中的标号
在汇编语言中,标号(Label)是一个非常重要的概念。标号本质上是一个符号,用于表示代码或数据的位置。通过使用标号,程序员可以更方便地引用和跳转到特定的代码段或数据段。
汇编语言中的标号规则:
1)允许使用字母、数字、下划线以及特殊符号@、$和?,但首字符不得为数字。
2)其长度限制在240个字符以内。
3)禁止使用指令名或其他保留关键字。
4)在同一作用域内,标号必须是唯一的。
### 标号的类型
1. **代码标号**:用于标记代码的位置,通常用于跳转指令(如 `JMP`、`CALL` 等)。
2. **数据标号**:用于标记数据的位置,通常用于数据定义指令(如 `DB`、`DW`、`DD` 等)。
### 代码标号
代码标号通常位于指令的前面,用于标记该指令的位置。例如:
start:
mov ax, 42
jmp start
在这个例子中,`start` 是一个代码标号,标记了 `mov ax, 42` 这条指令的位置。`jmp start` 指令会无条件跳转到 `start` 标号所在的位置。
### 数据标号
数据标号通常位于数据定义指令的前面,用于标记数据的位置。例如:
message db 'Hello, World!', 0
在这个例子中,`message` 是一个数据标号,标记了字符串 `'Hello, World!'` 的起始位置。程序可以通过 `message` 标号来访问这个字符串。
### 标号的用途
1. **跳转和调用**:通过标号,程序可以实现条件和无条件跳转,以及函数调用。
2. **数据访问**:通过标号,程序可以方便地访问数据段中的数据。
3. **模块化编程**:标号可以帮助组织代码,使其更具模块化和可读性。
### 示例
以下是一个简单的汇编程序,展示了代码标号和数据标号的使用:
.data
message db 'Hello, World!', 0
.code
start:
mov ax, @data
mov ds, ax
mov ah, 09h
lea dx, message
int 21h
mov ax, 4C00h
int 21h
end start
在这个例子中:
- `message` 是一个数据标号,标记了字符串 `'Hello, World!'` 的起始位置。
- `start` 是一个代码标号,标记了程序的入口点。
通过这些标号,程序可以方便地访问数据和控制程序的执行流程。
三、@@的使用
在MASM中,@@提供了便捷的本地标号功能。
例如:
xor eax, eax
test eax, eax
je @F ; 跳转到此条指令后的第一个@@的地址
mov ebx, 99h
@@ ; 此条指令前的第一个@@的地址
loop @B ; 建议:在同一@@作用域下,@@、@F与@B之间不要间隔太远
通过使用@@、@F和@B,程序员可以更灵活地控制代码的跳转,尤其是在循环和条件判断中。
四、数据定义
常量定义:
const
IDD_DIALOG1 equ 101 ; 定义一个值为101的常量
数据定义:
data db "%02x", 0 ; 定义一个字符串FormatStr
未初始化数据定义:
hInstance dd ? ; 定义一个未初始化的变量
在汇编语言中,数据定义用于在数据段中分配内存并初始化数据。常量定义用于定义不可更改的值,数据定义用于定义已初始化的数据,而未初始化数据定义用于定义未初始化的变量。这些定义有助于组织和管理程序中的数据。
五、全局变量
格式:
变量名 类型 初始值1, 初始值2, ...
变量名 类型 重复数量 dup(初始值1, ...)
示例:
.data
wHour dw ? ; 2字节,未初始化
wMinute dw 10 ; 2字节,初始化为10
hWnd dd ? ; 4字节,未初始化
Buffer dw 100 dup(1, 2) ; 2 * 100字节,初始化为1和2的重复序列
szBuffer byte 1024 dup(?) ; 1024字节,未初始化
szText db 'Hello, world!', 0 ; 12字节,字符串
szHello db 'Hello,', 0dh, 0ah, 'world!', 0 ; 包含换行符的字符串
在汇编语言中,全局变量定义用于在数据段中分配内存并初始化数据。变量名后面跟着类型和初始值或重复数量及初始值。这些定义有助于组织和管理程序中的数据。
数据类型及表示方式:
六、局部变量
声明局部变量的关键字为 `LOCAL`,它会在栈中开辟出相应大小的空间用以保存这个局部变量。
例如:
LOCAL hDlgEdt : HWND ; 4字节空间
LOCAL dwNameSize : DWORD ; 4字节空间
LOCAL szName[128] : BYTE ; 128字节空间
在汇编语言中,局部变量通过 `LOCAL` 关键字声明,并在栈中分配内存。每个局部变量后面跟着类型和大小。这些定义有助于在函数或代码块中管理临时数据。
七、结构体
定义结构体:
WNDCLASS struct
Style DWORD ?
LpfnWndProc DWORD ?
cbClsExtra DWORD ?
WNDCLASS ends
声明结构体:
data ?
stWndClass WNDCLASS <> ; 声明结构体
data
stWndClass WNDCLASS <1, 1, 1> ; 声明结构体并赋值
在汇编语言中,结构体用于定义复杂的数据结构。通过 `struct` 和 `ends` 关键字定义结构体,并在数据段中声明结构体变量。结构体变量可以不初始化,也可以在声明时进行初始化。这些定义有助于组织和管理复杂的数据结构。
八、结构体的访问
在MASM中访问结构体有三种方法:
1. **直接访问**:
mov eax, stWndClass.lpfnWndProc
2. **利用寄存器访问**:
mov esi, offset stWndClass
mov eax, [esi + WNDCLASS.lpfnWndProc]
注意:第二句是 `[esi + WNDCLASS.lpfnWndProc]` 而不是 `[esi + stWndClass.lpfnWndProc]`。
3. **使用 `assume` 伪指令预先定义寄存器访问**:
mov esi, offset stWndClass
assume esi: ptr WNDCLASS
mov eax, [esi].lpfnWndProc
assume esi: nothing
在汇编语言中,访问结构体成员可以通过直接访问、利用寄存器访问或使用 `assume` 伪指令预先定义寄存器访问。这些方法提供了灵活性和便利性,使得程序员可以根据具体需求选择最合适的方式来访问结构体成员。
九、获取变量地址
对于全局变量,它的地址在编译时已经由编译器确定,其用法如下:
mov 寄存器, offset 变量名
如果要在 `invoke` 伪指令的参数中用到一个局部变量的地址,该怎么办呢?参数中是不可能写入 `lea` 指令的,用 `offset` 也是不对的。MASM 对此有一个专用的伪操作符 `addr`,其格式为:
addr 局部变量名或全局变量名
注意:
mov eax, addr 局部变量名 ; 错误用法
假设在一个子程序中有如下 `invoke` 指令:
invoke Test, eax, addr szHello
其中 `Test` 是一个需要两个参数的子程序,`szHello` 是一个局部变量,会发生什么结果呢?编译器会把 `invoke` 伪指令和 `addr` 翻译成下面这个模样:
lea eax, [ebp-4]
push eax ; 参数2: addr szHello
push eax ; 参数1: eax
call Test
我们可以发现,在 `push` 第一个参数 `eax` 之前,`eax` 的值已经被 `lea eax, [ebp-4]` 指令覆盖了!
所以,当在 `invoke` 中使用 `addr` 伪操作符时,注意在它的前面不能用 `eax`,否则 `eax` 的值会被覆盖掉。当然,`eax` 在 `addr` 的后面的参数中用是可以的。幸亏 MASM 编译器对这种情况有如下错误提示:
error A2133: register value overwritten by INVOKE
这个错误提示提醒我们在使用 `addr` 伪操作符时要注意寄存器的使用顺序,避免不必要的问题。
十、函数
函数声明:
DlgProc proto :HWND, :UINT, :WPARAM, :LPARAM
函数定义:
FunName proc hDlg:HWND, dwVar:DWORD
ret
FunName endp
在汇编语言中,函数通过 `proto` 伪指令进行声明,并通过 `proc` 和 `endp` 关键字进行定义。函数声明指定了函数的名称和参数类型,而函数定义则包含了函数的具体实现。这些定义有助于组织和管理程序中的函数调用。
十一、分支与循环
在汇编语言中,分支和循环结构可以通过 `.if`、`.while`、`.break` 和 `.continue` 等伪指令来实现。
1. **条件分支**:
.if 表达式1
表达式1为“真”要执行的指令
.endif
当 `表达式1` 为真时,执行指定的指令。
2. **循环**:
.while 条件测试表达式
指令
[.break [.if 退出条件]]
[.continue]
.endw
当 `条件测试表达式` 为真时,执行循环体内的指令。可以使用 `.break` 和 `.if` 组合来提前退出循环,或者使用 `.continue` 跳过当前循环的剩余部分,直接进行下一次循环。
这些伪指令提供了类似于高级语言中的控制结构,使得汇编代码更具可读性和可维护性。
十二、内联汇编
在C/C++中,可以使用内联汇编来嵌入汇编代码。内联汇编有两种形式:块内联汇编和行内联汇编,二者可以交叉使用。
示例:
#define EXAMPLE_CODE asm
/* 内联汇编宏定义示例 */
EXAMPLE_CODE {
pushad
popad
}
在定义一个具有多行汇编指令的宏时,一定要采取二者交叉使用的模式,否则会引起编译错误。块内联汇编使用 `asm { ... }` 的形式,而行内联汇编使用 `asm ...` 的形式。通过合理使用这两种形式,可以更灵活地在C/C++代码中嵌入汇编指令,从而实现更高效的代码执行。
十三、裸函数的使用
裸函数是一个没有任何可执行代码的空函数,它在内存中仅仅是一条地址信息。裸函数使用关键字 `__declspec(naked)` 定义,一个可运行的最简单的裸函数如下所示:
void __declspec(naked) TestFun()
{
__asm ret
}
裸函数的特点是编译器不会为函数生成任何 prologue 或 epilogue 代码,这意味着函数体内必须手动编写所有的汇编代码,包括函数返回指令。裸函数通常用于需要精细控制函数执行流程的场合,例如编写中断处理程序或性能敏感的代码。
标签:标号,汇编,定义,eax,汇编程序,mov,Win32,第五节,函数 From: https://blog.csdn.net/linshantang/article/details/141366893