引言:
本文介绍是宏汇编语言基础以及MASM宏汇编编译器基本使用方法,不同汇编语言的指令集都大差不差,主要是学会指令的语法及使用,会使用反汇编语言来分析目标程序就行,不必过于纠结不用汇编之间的差异。以下是本人的学习记录,如有问题,请各位博友指出。
1.汇编语言的介绍
汇编语言就是机器语言(0或1)的人类可读版本。它用简短的助记符(如MOV、ADD、JMP)来代替二进制指令,用标号来代替内存地址。
比如,同样是把数字5存入寄存器,机器语言是:
10110000 00000101而汇编语言是:
MOV AL, 5汇编语言是基于苦涩、难懂的机器语言而改进的便于人理解的语言,并且现在的很多硬件设备都是用汇编语言作为主要语言进行程序烧入的,汇编语言是计算机语言史上的一座里程碑。
那么问题来了:为什么不直接用高级语言?因为汇编语言离硬件最近——它和机器语言几乎是一一对应的关系。每一行汇编代码,都直接对应一条CPU指令。这意味着:你用汇编写的每一行代码,都在精确指挥硬件如何工作。这种精确控制,是任何高级语言都无法提供的。
2.汇编语言的“三足鼎立”
| 架构 | 典型应用 | 特点 |
|---|---|---|
| x86(Intel语法) | Windows、传统PC | 目标在前,源在后:MOV eax, 1 |
| x86(AT&T语法) | Unix/Linux/GCC内联汇编 | 源在前,目标在后:movl $1, %eax |
| ARM | 手机、嵌入式、M系列Mac | 精简指令集,移动时代王者 |
本文介绍的是x86 Intel语法。
3. 基础知识
3.1 进制
我们平时常用的进制有二进制、八进制、十进制、十六进制,它们的表示如下表。
| 名称 | 表示 | 例子 |
|---|---|---|
| 二进制 | 0、1 | 1010 |
| 八进制 | 0~7 | 12 |
| 十进制 | 0~9 | 10 |
| 十六进制 | 0~9、A~F | A |
注意:十六进制的A~F对于10进制的10~15,后面进制转换会用到。
3.2 进制转换
3.2.1 位权
一个数从右向左,每一位的权值都是“进制的不同次方”。
例如十进制:
123从右往左:
1 2 3 ↓ ↓ ↓ 10² 10¹ 10⁰所以:
123 = 1×10² + 2×10¹ + 3×10⁰ = 100 + 20 + 3 = 1233.2.2 任意进制转换为十进制
核心公式:
N = aₙ×bⁿ + aₙ₋₁×bⁿ⁻¹ + ... + a₁×b¹ + a₀×b⁰其中:
b:目标数字的原进制a:每一位上的数字- 从最右边开始,位权从
b⁰开始
简单记忆:
从右往左编号 0、1、2、3……,每一位乘以“进制的对应次方”,最后全部相加。
例如:
二进制转十进制
110101₂从右向左标记:
1 1 0 1 0 1 5 4 3 2 1 0因此:
110101₂ = 1×2⁵ + 1×2⁴ + 0×2³ + 1×2² + 0×2¹ + 1×2⁰ = 32 + 16 + 0 + 4 + 0 + 1 = 53所以:
110101₂ = 53₁₀八进制转十进制
753₈从右向左标记:
7 5 3 2 1 0计算:
753₈ = 7×8² + 5×8¹ + 3×8⁰ = 7×64 + 5×8 + 3 = 448 + 40 + 3 = 491所以:
753₈ = 491₁₀十六进制转十进制
3F2₁₆其中:
F = 15从右向左标记:
3 F 2 2 1 0计算:
3F2₁₆= 3×16² + 15×16¹ + 2×16⁰ = 3×256 + 15×16 + 2 = 768 + 240 + 2 = 1010所以:
3F2₁₆ = 1010₁₀3.2.3 十进制转其他进制
十进制转换为二进制、八进制、十六进制,需要使用“除基取余法”。
例如:
十进制转二进制
例如:
45₁₀不断除以 2:
45 ÷ 2 = 22 …… 1 22 ÷ 2 = 11 …… 0 11 ÷ 2 = 5 …… 1 5 ÷ 2 = 2 …… 1 2 ÷ 2 = 1 …… 0 1 ÷ 2 = 0 …… 1余数从下往上读取:
101101因此:
45₁₀ = 101101₂十进制转八进制
83₁₀不断除以 8:
83 ÷ 8 = 10 …… 3 10 ÷ 8 = 1 …… 2 1 ÷ 8 = 0 …… 1从下往上读取:
123因此:
83₁₀ = 123₈十进制转十六进制
687₁₀不断除以 16:
687 ÷ 16 = 42 …… 15 42 ÷ 16 = 2 …… 10 2 ÷ 16 = 0 …… 2将余数转换成十六进制:
15 = F 10 = A从下往上读取:
2AF所以:
687₁₀ = 2AF₁₆3.3 数据宽度及表示范围
| 单位 | 所占位数 | 字节数 | 十六进制范围 |
| 字节(byte) | 8 | 1 | 0~FF |
| 字(word) | 16 | 2 | 0~FFFF |
| 双字(dword) | 32 | 4 | 0~FFFFFFFF |
| 八字节(qword) | 64 | 8 | 0-FFFFFFFFFFFFFFFF |
3.4 大、小端字节序
字节序(Byte Order)是指多字节数据在内存中的存储顺序,主要有两种类型:大端字节序(Big-Endian)和小段字节序(Little-Endian)。
大端字节序
大端字节序是指数据的高位字节存储在内存的低地址端,而低位字节存储在内存的高地址端。大端字节序符合我们的阅读习惯,从左至右且符号位放在第一位,判断正负数的效率较高,但逻辑运行相对低。
小端字节序
小端字节序是指数据的低位字节存储在内存的低地址端,而高位字节存储在内存的高地址端。小端字节序在数据类型的转换时效率较高因为直接取低位数据值即可。
大端字节序和小端字节序的存储顺序示意图如下。
存储十六进制0x12345678
应用场景
- 计算机内部处理:大多数现代计算机的内部处理使用小端字节序,因为这种方式能更有效地利用计算机电路。
- 网络传输:在网络传输中,通常使用大端字节序。如TCP/IP协议使用大端字节序,因此在网络通信中需要考虑字节序的转换问题,以确保发送方和接受方对多字节数据的解释一致。
我们的计算机通常是小端字节序存储数据。
可通过一下程序来判断字节自己计算机的存储方式。
取值比较法
#include<windows.h> #include<stdio.h> int main(){ DWORD dwSmallNum=0x01020304; if(*(BYTE*)&dwSmallNum==0x04) printf("Small Sequence.\r\n"); else printf("Big Sequence.\r\n"); return 0; }直接转换比较法
#include<stdio.h> #include<winsock2.h> #pragma comment(lib,"win2_32") int main(){ DWORD dwSmallNum=0x01020304; if(dwSmallNum==htonl(dwSmallNum)) printf("Big Sequence.\r\n"); else printf("Small Sequence.\r\n"); return 0; }3.5 编译器安装
工欲善其事,必先利其器,本文用到的是32位的宏汇编编译器。工具链接:百度网盘 请输入提取码百度网盘为您提供文件的网络备份、同步和分享服务。空间大、速度快、安全稳固,支持教育网加速,支持手机端。注册使用百度网盘即可享受免费存储空间https://pan.baidu.com/s/1Nz-tZKrg5t_T1sJU8H-V1g?pwd=8tiv将压缩包解压后分别将编译器文件夹绝对路径和文件夹中的bin文件夹绝对路径添加到环境变量中即可在cmd中正常使用。
3.6 汇编语言程序格式
操作码与操作数
汇编语言程序本质上由一条条指令构成,而一条指令通常由操作码和操作数(地址码)组成。
操作码用于说明“要做什么操作”。
比如:
MOV:数据传送ADD:加法SUB:减法JMP:无条件跳转CMP:比较
操作数用于说明“对谁进行操作”或者“操作的数据在哪里”。
比如:
MOV AX BX
│ │ │
│ │ └── 源操作数
│ └──────── 目标操作数
└────────────── 操作码
表示将BX中的数据传送到AX中。
标识符
标识符包括标号(Label)和名字(Name)都是由程序员自定义,主要用于表示程序中某个存储位置的逻辑地址,但使用场景不同。
| 对比 | 标号 | 名字 |
|---|---|---|
| 使用语句 | 执行性语句 | 说明性语句 |
| 分隔方式 | 通常用冒号: | 通常用空格或制表符 |
| 主要表示 | 指令在主存中的逻辑地址 | 变量、段、子程序等的逻辑地址 |
| 主要作用 | 指明分支、循环等的目的地址 | 标识变量、段、子程序等 |
| 示例 | LOOP: | DATA DB 10 |
如:
LOOP: ADD AX, BX JMP LOOP 其中 LOOP 是标号。 可以理解为: LOOP: ↓ 指向 ADD AX,BX 所在的逻辑地址 执行JMP LOOP就是跳转到 LOOP 对应的地址。DATA DB 10 DATA → 名字 DB → 说明性语句中的操作 10 → 数据 DATA 表示这段数据所在的逻辑地址。标识符最多由31个字母、数字以及规定的特殊符号(如_、$、?、@)组成,且不能由数字开头,也不能是汇编程序采用的保留字。保留字是编程语言本身需要使用的各种具有特殊含义的标识符,也称关键字,关键字详细可见下表。
| 类型 | 示例 | 作用 |
|---|---|---|
| 硬指令助记符 | MOV、ADD、SUB、JMP | 表示 CPU 执行的指令 |
| 伪指令助记符 | DB、DW、SEGMENT、ENDS | 指示汇编器如何处理程序 |
| 寄存器名 | AX、BX、CX、DX | 表示 CPU 寄存器 |
| 其他特殊符号/关键字 | PROC、ENDP、ASSUME等 | 具有特定语法含义 |
执行性语句:
标号: 硬指令助记符 目标操作数,源操作数 ;注释
LOOP: MOV AX, BX ; 将BX的内容送入AX说明性语句:
名字 伪指令助记符 参数,参数,....... ;注释
msg db"hello, assembly!",13,10,0 ;定义一个字节类型的字符串变量,变量名为msg(message的缩写)。13相当于回车符\r,10相当于换行符\n,0相当于字符串结束符(NULL终止符)包含伪指令include
将常用的常量定义、过程说明、共享的子程序库等内容进行声明(相当于c语言中包含头文件的作用)。
include io32.inc ;包含I/O库文件,该库包含dismsg,dispint,readmsg,exit等函数(整个语句类似于从语言的# include <stdio.h>)段的简化定义
段是把程序按照不同用途划分出来的一块逻辑区域。
一个汇编程序通常会划分为不同的段,例如:
汇编程序 │ ├── 代码段(CODE) │ └── 存放程序指令 │ ├── 数据段(DATA) │ └── 存放已经初始化的数据 │ ├── 未初始化数据段(DATA?) │ └── 存放未初始化的数据 │ └── 堆栈段(STACK) └── 存放堆栈数据“段的简化定义”指的是使用.DATA、.CODE、.STACK等简化段定义伪指令来定义程序中的各个段,不需要像完整段定义那样写SEGMENT和ENDS。
数据段定义伪指令.DATA创建一个数据段,定义可读可写的变量等
代码段定义伪指令.CODE创建一个代码段,保留需要执行的可执行性语句
堆栈段定义伪指令.STACK创建一个堆栈段。
更多详情可参考汇编语言教程 | 汇编语言从入门到精通 | w3schools 中文网
3.7 编译器的简单使用
这是本次的示例代码:
;eg01.asm in windows console for MASM include io32.inc ;包含I/O库文件,该库包含dismsg,dispint,readmsg,exit等函数(整个语句类似于从语言的# include <stdio.h>) .data ;数据段开始标志 msg db"hello, assembly!",13,10,0 ;定义一个字节类型的字符串变量,变量名为msg(message的缩写)。13相当于回车符\r,10相当于换行符\n,0相当于字符串结束符(NULL终止符) .code ;代码段开始标志 start: ;程序起始执行位置 mov eax,offset msg ;使用offset这个操作数计算msg的偏移量,并将值传递给eax call dispmsg ;调用dispmsg这个库函数来显示eax中的字符串 exit 0 ;主程序结束位置 end start ;汇编代码结束位置Notepad2.exe是MASM目录中的asm代码编辑器,可使用该编辑器直接编写汇编代码。
快速开发方法
- 进入MASM目录,可从cmd窗口切换,也可直接双击WIN32.bat。
- 生成可执行文件
make32 文件名 //也就说汇编代码.asm的前面部分自定义编译
先生成目标模块文件(.obj)
ml /c /coff 汇编文件 #/c 只编译,不链接 #/coff 生成 COFF 格式 .obj生成可执行文件(.exe)
link32 /subsystem:console 目标文件 /libpath:bin #/subsystem:console,指定子系统为控制台,生成控制台程序 #/libpath:bin,指定库文件搜索路径,告诉链接器到 bin 目录寻找 .lib 库文件调试程序
指令:
ml /c /coff /Fl /Zi 汇编文件 #/Fl 生成 汇编列表文件 .lst,.lst 列表文件通常可以看到源代码、地址、机器码等汇编信息。 #/Zi 生成调试信息,用于调试程序,让调试器能够把机器指令和源代码对应起来,便于单步执行、查看变量等。link32 /subsystem:console eg01.obj /debug /libpath:bin #/debug 调试信息,在生成的程序中保留/生成调试信息,便于调试3.8 指令集
CPU 的指令集架构(ISA)可以按照指令设计思想分为两种典型类型:
- CISC(Complex Instruction Set Computer):复杂指令集计算机
- RISC(Reduced Instruction Set Computer):精简指令集计算机
二者的主要区别在于:
CISC 强调让单条指令完成更多功能,让软件轻松,硬件受累;RISC 强调使用简单、规整的指令,通过多条指令组合完成复杂任务,让硬件轻松,软件多写几条。
CISC
| 架构 | 代表厂商/产品 | 特点 |
| x86/x86-64 | Intel、AMD、海光、兆芯 | 性能强、兼容性好、软件生态丰富;但功耗较高、硬件复杂 x86的32位版本,现已逐步被64位x86-64取代 |
| IA--32 | Intel早期32位处理器(如Pentium) | x86的32位版本,现已逐步被64位x86-64取代 |
RISC
| 架构 | 代表厂商/产品 | 特点 |
| ARM | 苹果M系列、高通骁龙、华为鲲鹏、飞腾 | 低功耗、高效能、移动设备霸主;服务器领域也在快速扩展 |
| RISC-V | 阿里平头哥、国芯科技、SiFive | 完全开源、模块化、无版权费;生态正在快速建设中 |
| MIPS | 龙芯(早期) | 简洁、优化方便;但生态逐渐萎缩,龙芯已转向自研LoongArch |
| Alpha | 申威 | 高性能,主要用于超算和特定领域 |
3.9 计算机的基本结构
微型计算机系统的基本组成
- 硬件:物理设备
- 软件:程序和文档
现代计算机基本组成结构
汇编语言程序员将硬件抽象为:寄存器、存储器地址和输入输出地址。
3.10 寄存器
3.10.1 通用寄存器
通用寄存器(General Purpose Registers)是最常用的寄存器,用于存放运算数据和临时结果。
x86提供了8个常用的x86通用寄存器
| 32 位 | 16 位 | 高 8 位 | 低 8 位 | 主要用途 |
|---|---|---|---|---|
| EAX | AX | AH | AL | 累加器,存放函数返回值、算术运算结果 |
| EBX | BX | BH | BL | 基址寄存器,常用于存放内存基地址 |
| ECX | CX | CH | CL | 计数器,常用于循环计数和移位 |
| EDX | DX | DH | DL | 数据寄存器,存放乘除法的高位结果 |
| ESI | SI | — | — | 源变址寄存器,字符串操作的源地址 |
| EDI | DI | — | — | 目的变址寄存器,常用于数组和字符串操作 |
| EBP | BP | — | — | 基址指针,常用于定位栈帧 |
| ESP | SP | — | — | 栈指针,指向当前栈顶 |
3.10.2 段寄存器
段寄存器用于指定当前使用的内存段。
常见的16位段寄存器有6 个:
| 段寄存器 | 名称 | 主要用途 |
|---|---|---|
| CS | Code Segment(代码段寄存器) | 指向当前正在执行的代码 |
| DS | Data Segment(数据段寄存器) | 指向程序的数据 |
| SS | Stack Segment(栈段寄存器) | 指向栈空间 |
| ES | Extra Segment(附加段寄存器) | 可用于额外的数据段 |
| FS | Extra Segment(附加段寄存器) | 现代系统中具有特殊用途 |
| GS | Extra Segment(附加段寄存器) | 现代系统中具有特殊用途 |
3.10.3 指针与变址寄存器
这些32位寄存器主要用于访问内存,存放内存地址:
| 寄存器 | 名称 | 主要作用 |
|---|---|---|
| EIP | Instruction Pointer(指令指针寄存器) | 指向下一条要执行的指令 |
| ESP | Stack Pointer(栈指针寄存器) | 指向当前栈顶 |
| EBP | Base Pointer(基址指针寄存器) | 定位当前函数的栈帧 |
| ESI | Source Index(源变址寄存器) | 保存源数据的地址或索引 |
| EDI | Destination Index(目的变址寄存器) | 保存目标数据的地址或索引 |
3.10.4 标志寄存器
标志寄存器(EFLAGS)是一个 32 位寄存器,每个比特位代表一个状态标志(Flag)。
你不能直接读写整个 EFLAGS,但 CPU 会根据运算结果自动更新这些标志位,条件跳转指令则根据标志位决定是否跳转。
常见标志位:
| 标志位 | 名称 | 作用 |
|---|---|---|
| CF | Carry Flag(进位标志) | 无符号运算产生进位或借位时置 1 |
| PF | Parity Flag(奇偶标志) | 结果低 8 位中 1 的个数为偶数时置 1 |
| AF | Auxiliary Carry Flag(辅助进位标志) | 低 4 位向高 4 位产生进位或借位时置 1 |
| ZF | Zero Flag(零标志) | 运算结果为 0 时置 1 |
| SF | Sign Flag(符号标志) | 运算结果最高位为 1 时置 1 |
| OF | Overflow Flag(溢出标志) | 有符号运算发生溢出时置 1 |
| DF | Direction Flag(方向标志) | 控制字符串操作的地址增减方向 |
| IF | Interrupt Enable Flag(中断允许标志) | 控制是否允许可屏蔽中断 |
| TF | Trap Flag(陷阱标志) | 用于单步调试 |
参考:汇编语言 – 寄存器 | 菜鸟教程