intel-32基础语法(上)
# 前言
汇编语言多种多样,不同于其他语言,汇编指令对接的是硬件,所以不同厂家,甚至相同的厂家的不同芯片都有不同的汇编指令集。
因此花时间学完所有的汇编指令显得不必要,只要对主流的汇编语法学习然后融会贯通即可。
常见学习的汇编:
1、8086汇编,intel-16,早期较为基础的汇编指令,也是入门常用的汇编学习版本,大学教材《王爽汇编xx》
2、intel-32位 ,intel的32位芯片
3、intel-64,现在主流的intel芯片版本
4、ARM-64,ARM当前主流的版本
⚠️ 注意:
本篇以C++语法对比介绍intel-32汇编指令集(无特殊说明的,下面介绍的汇编指令均为 intel-32 指令集)
# 基本结构
# Hello World
Hello World 示例对比
1、C++ 显示字符串“ Hello World”
#include <iostream> // 引入支持输入输出的库
using namespace std; // 命名空间
int main(){ // 程序开始执行入口函数
cout << "Hello World"; // cout 输出
return 0;
}
C++在线编译(菜鸟工具) (opens new window)
2、intel32汇编语言显示字符串“ Hello World”
section .text
global _start
_start:
mov edx,len
mov ecx,msg
mov ebx,1
mov eax,4
int 0x80
mov eax,1
int 0x80
section .data
msg db 'Hello, world!', 0xa
len equ $ - msg
链接:intel32汇编在线编译 (opens new window)
上面结果都是打印一个 'Hello, world!' 但是,
显然汇编语法较为复杂,下面将先从结构上介绍他们的区别。
(具体 ax、bx、cx、dx、int 等指令后面再介绍)
# 语法结构
改造C++ Hello World示例语法:
#include <iostream> // 引入支持输入输出的库
using namespace std; // 命名空间
// 1、data段: 被定义不可修改的数据常量
const string msg = "Hello World";
// 2、text段:程序代码
int main();
int main(){ // 3、程序入口函数
cout << msg; // cout 输出
// 4、程序返回
return 0; // ret
}
汇编代码: Hello World
; 1、data段: 被定义不可修改的数据常量
section .data
msg db 'Hello, world!', 0xa ;要打印的字符串
len equ $ - msg ;字符串的长度,尾地址($)- 起始地址(msg[0])= 字符串长度
; 2、text段:程序代码
section .text
global _start ;必须为链接器(ld)声明
_start: ; 3、程序入口函数:告诉链接器入口点
mov edx,len ;消息长度
mov ecx,msg ;写消息
mov ebx,1 ;文件描述符 (stdout)
mov eax,4 ;系统调用号 (sys_write)
int 0x80 ;调用内核
mov eax,1 ;系统调用号 (sys_exit)
; 4、程序中断
int 0x80 ;调用内核
对比两段代码(大概类比,方便记忆):
1、data段:
被定义不可修改的数据常量,其中数据 **msg db ***** 等同于C++ 常量 const string msg
2、text段:声明
程序代码,比如入口函数声明 global _start 等同于C++ main函数声明 int main();
3、text段:程序实现
程序入口函数
代码实现 _start: 等同于 C++ main函数 实现 int main(){ *** }
4、text段:执行中断或退出
一个程序的函数通常会在使用完后中断退出
mov eax,1 ;系统调用sys_exit,相当main函数的退出,也即是进程退出
返回:
mov eax,4
int 0x80 ;调用内核中断
上面两句相当于函数(过程)的返回吧,
return :C++中当前函数执行完毕
5、屏幕输出
cout : c++屏幕输出
mov ebx,1 ;文件描述符 (stdout)
6、注释
intel32汇编语言 行注释以分号(;)开头
; 注释信息
C++中行注释
// 注释信息
(还有其他多种注释方法)
# intel32程序三个段结构
# 1、data段
数据(data)段被用于声明初始化的数据或常数。此数据在运行时不会更改。您可以在段中声明各种常量值,文件名或缓冲区大小等。
声明数据段的语法是-
section.data
# 2、bss 段
在bss段用于声明变量。声明bss段的语法是-
section .bss ;未初始化的数据
num resb 8
# 3、text段
代码段被用于保持实际的代码。该段必须以全局声明**_start**开头,该声明告诉内核程序从何处开始执行。
声明代码段的语法是-
section.text
global _start
_start:
# 寄存器
# 常见寄存器
寄存器 | 16位 | 32位 | 64位 |
---|---|---|---|
累加寄存器 accumulator | AX | EAX | RAX |
基址寄存器 base | BX | EBX | RBX |
计数寄存器 count | CX | ECX | RCX |
数据寄存器 data | DX | EDX | RDX |
堆栈基指针 Base Pointer | BP | EBP | RBP |
变址寄存器 Source Index | SI | ESI | RSI |
堆栈顶指针 Stack Pointer | SP | ESP | RSP |
指令寄存器 Instruction Pointer | IP | EIP | RIP |
AH&AL=AX(accumulator):累加寄存器
BH&BL=BX(base):基址寄存器
CH&CL=CX(count):计数寄存器
DH&DL=DX(data):数据寄存器
SP(Stack Pointer):堆栈指针寄存器
BP(Base Pointer):基址指针寄存器
SI(Source Index):源变址寄存器
DI(Destination Index):目的变址寄存器
IP(Instruction Pointer):指令指针寄存器
CS(Code Segment)代码段寄存器
DS(Data Segment):数据段寄存器
SS(Stack Segment):堆栈段寄存器
ES(Extra Segment):附加段寄存器
⚠️
1、你会发现寄存器指令名称基本是在16位基础上衍生而来,比如32位 EAX是在16 位AX前加E,64 位 RAX是在16 位AX前加R
2、H是高位缩写,L是低位缩写。比如 AX 由AH 高位和AL低位组合,这是为兼容8位寄存器而来
3、篇头demo中下面代码什么意思?
_start: ; 3、程序入口函数:告诉链接器入口点
mov edx,len ;消息长度
mov ecx,msg ;写消息
mov ebx,1 ;文件描述符 (stdout)
mov eax,4 ;系统调用号 (sys_write)
int 0x80 ;调用内核
拆分解释一下:
; 程序入口函数:告诉链接器入口点,加这个好理解,就是main函数
_start:
;比如C语法中 int a = 1; 就默认处理好了变量a的数据长度,通常是4字节,代码底层帮你申请了对应大小的空间,通常使用 alloc 分配。
;而在汇编中可以通过 dx 告诉编译器当前对象的需要分配的内存大小,
mov edx,len ;消息长度
; int a = 1 中,赋值1给变量a的过程在汇编中可以使用 cx 这个指令
; mov 赋值,msg 数据, cx 指向真实的物理地址,将内容写入的地方
mov ecx,msg ;写消息
;这个就是打印指令,也即是输出指令,cout、print、等等类似
;在bx寄存器中写入1就是输出信息
mov ebx,1 ;文件描述符 (stdout)
;ax中写入100(4)即是相当于函数的返回指令,告诉CPU下面函数过程要做的动作是执行后返回
mov eax,4 ;系统调用号 (sys_write)
;int 不是c中的int,而是 Interrupt,中断的意思,而0x80是intel-32中调用linux中系统命令,可以理解为system_call,也就是系统函数调用
int 0x80 ;调用内核
# 汇编命令
列举几个汇编命令:
指令 | 作用 | |
---|---|---|
ORG | 在程序执行的时候,会告诉编译器将这些指令转载到内存的哪个位置 | |
JMP (jump) | 跳转。类似于C中的 go to | |
entry() | JMP 指令的跳转目的地 | |
DB(data byte) | 向文件中直接写入一个字节的指令 | |
DW(data word) | 写入一个字,两个字节 | |
DD(data double-word) | 写入两个字,4个字节 | |
RESB(reserve byte) | 与 DB相反,指定的地址清空,保留备用 | |
RESW(reserve data double-word) | 与 DW相反 | |
MOV(move) | 移动、赋值,等等动作 | |
ADD(add) | 加法指令 | |
CMP(compare) | 比较指令。 | |
INT(Interrupt) | 中断指令,用来调用BIOS中函数的指令 | |
HLT(halt) | 停止。让CPU进入待机状态,只要外部发生变化,CPU就会醒过来。 | |
JC(jump if carry) | 标志,INF调用函数后,没有错,进位标志为0,有错,进位标志为1 | |
JNC(jump if not carry) | 判断指令:进位为0就跳转 | |
JAE(jump if above or equal) | 判断指令:大于等于时跳转 | |
JBE(jump if below or equal) | 判断指令:小于等于则跳转 | |
RET(return) | 返回 | |
XCHG (exchange) | 交换指令 | |
push | 栈:进栈,或叫压栈指令 | |
pop | 栈:出栈指令 | |
sub、sbb、dec、neg、cmp | 减法指令 | |
mul、imul | 乘法指令 | |
div、idiv | 除法指令 | |
not、and、or、xor、test | 逻辑运算指令 | |
ROL、ROR、RCL、RCR | 循环移位指令 | |
LOOP、LOOPE/LOOPZ、LOOPNE/LOOPNZ、JCXZ | 循环指令 | |
其他详见汇编指令梳理篇章 | ||
# 变量
# define:变量内存分配、创建(alloc)
NASM提供了各种定义指令来为变量保留存储空间。
define assembler指令用于分配存储空间。
在C++中:
int a = 3;
double b = 1.345;
byte z = 22;
char x = 'x';
而汇编中,变量示例:
str DB 'xyz'
num DW 354534
neg_num DW -323
big_num DQ 119287337644
real_num DD 1.23456
real_num2 DQ 123.456
其中内存分配的define指令:
指令 | 目的 | 储存空间 |
---|---|---|
DB | 定义字节 | 分配1个字节 |
DW | 定义字 | 分配2个字节 |
DD | 定义双字 | 分配4个字节 |
DQ | 定义四字 | 分配8个字节 |
DT | 定义十个字节 | 分配10个字节 |
# 示例:输出字符串 xyz
section .text
global _start ;链接器入口声明
_start: ;链接器执行入口
mov edx,3 ;定义长度
mov ecx,str1 ;写入数据
mov ebx,1 ;基地址(base)寄存器, 在内存寻址时存放基地址
mov eax,4 ;输出信息
int 0x80 ;中断
mov eax,1 ;退出exit
int 0x80 ;中断
section .data
str1 DB 'xyz'
结果:
xyz
1、字符的每个字节以十六进制形式存储为其ASCII值
2、数据类型转换为其等效的16位二进制数,并以十六进制数形式存储
3、使用小尾存储顺序(高字节存放内存高位,低字节在低位)
4、负数以补码形式表示
# reserve:变量内存释放(release)
相应的,上面define给对应内存分配空间,而reserve相反,是给对应内存地址清理空间,可以理解为 release
reserve指令用于为未初始化的数据保留空间。reserve指令采用单个操作数,该操作数指定要保留的空间单位数。每个define指令都有一个相关的reserve指令。
保留指令有五种基本形式-
指令 | 目的 |
---|---|
RESB | 保留一个字节 |
RESW | 保留字 |
RESD | 保留双字 |
RESQ | 保留四字 |
REST | 保留十个字节 |
# 示例:申请空间并打印
section .text
global _start ;必须为链接器(ld)声明
_start: ;告诉链接器入口点
;1、打印固定字符串 “我的年龄:”
mov edx,len ;设置数据长度
mov ecx,msg ;赋值数据
mov ebx,1 ;打印信息
mov eax,4 ;执行函数标记4
int 0x80 ;调用内核call函数执行
;2、打印存储后的数据 “9”
mov eax,'9'
mov [sum], eax ;将数据9存储到申请的内存中
mov edx,1
mov ecx,sum ;从 sum中取数据存入
mov ebx,1
mov eax,4
int 0x80
;3、退出
mov eax,1 ;退出进程sys_exit
int 0x80 ;调用内核call函数执行退出
section .data
msg db '我的年龄:',0xA,0xD ;换行 0xA,0xD 。要打印的字符串
len equ $ - msg ;字符串的长度
segment .bss
sum resb 1 ;申请1字节空间
结果
我的年龄:
9
# 常量
在C++中我们使用过 const、#define 等 定义一个常量,汇编中类似
# 常量定义
intel 32汇编指令 | C++ | 含义 |
---|---|---|
EQU (equ) | const (比如:const int a = 1;) | 不可变常量赋值 |
%assign | static | 静态变量,可修改 |
%define | #define | 单行宏,代码替换 |
%macro - %endmacro | 多行宏,代码替换 |
多行宏:
%macro 宏名称 参数数量
push ebp
mov ebp,esp
sub esp,%1
%endmacro
指令区分大小写
# 示例
1、C++示例:
#include <iostream>
using namespace std;
// 常量
const int i_x = 3;
// 宏
#define s_y "hello"
// 静态变量
static double d_z = 1.20;
int main()
{
cout << "不可修改 i_x = " << i_x << endl;
cout << "不可修改 s_y = " << s_y << endl;
d_z = d_z + 3.14;
cout << "可修改 d_z = " << d_z << endl;
}
结果
不可修改 i_x = 3
不可修改 s_y = hello
可修改 d_z = 4.34
# 汇编EQU示例
将通用指令固定数据定义为常量,方便使用
sys_exit equ 1 ;退出
sys_write equ 4 ;函数过程写
sdt_in equ 0 ;输入
sdt_out equ 1 ;输出
sys_call equ 0x80 ;调用
section .text
global _start
_start:
mov eax, sys_write
mov ebx, sdt_out
mov ecx, msg1
mov edx, len1
int sys_call
mov eax,sys_exit
int sys_call
section .data
msg1 db '我是第一行信息!',0xA,0xD ;换行 0xA,0xD
len1 equ $ - msg1
# 汇编宏示例
将上面示例使用宏修改:
;使用字符串变量
%macro set_string 1
mov ecx, %1
mov edx, len1
%endmacro
;调用并输出
%macro run_out 0
mov eax, 4
mov ebx, 1
int 0x80
%endmacro
;退出
%macro return 0
mov eax,1
int 0x80
%endmacro
section .text
global _start
_start:
set_string msg1
run_out
return
section .data
msg1 db '使用宏修改!'
len1 equ $ - msg1
退出的汇编代码直接使用 return 替换,这样更容易记忆。