从零开始学汇编

学习汇编有利于理解c语言本质,也是研究内核必备,同时可用户逆向领域,比如病毒与反病毒,外挂与反外挂,破解与反破解

learnNasm

寄存器详解

通用寄存器

数据寄存器 解释 备注
EAX(Accumulator) 累加寄存器 在乘法和除法指令中被自动使用;在Win32中,一般用在函数的返回值中。
EBX(Base) 基址寄存器 DS段中的数据指针
ECX(Count) 计数寄存器 在循环指令(LOOP)或串操作中,ECX用来进行循环计数,每执行一次循环,ECX都会被CPU自动减一;c++中保存this
EDX(Data) 数据寄存器
指针变址寄存器 解释 备注
EBP(Base Pointer) 扩展基址指针寄存器 SS段中堆栈内数据指针。EBP由高级语言用来引用参数和局部变量,通常称为堆栈基址指针寄存器。
ESP(Stack Pointer) 堆栈指针寄存器 表示栈顶指针,指向栈顶地址.与SS相配合使用
ESI(Source Index) 源变址寄存器 默认段地址和DI一样,在DS中.和DS联用.
EDI(Destination Index) 目的指针寄存器 一般情况下与ds联用,来确定某个储存单元的地址
  1. sp和bp段地址默认在SS中
  2. sp指向栈顶元素地址.有自加和自减能力,而bp没有.但是bp可以定位栈中某个元素的物理地址.

DI和SI这两个属于变址寄存器.可以和bx.bp联用,但是和bx连用时,段地址在DS中,和bp联用时,段地址在SS中.也可以单独使用,单独使用时,段地址默认在DS中,想要越段使用,加上段前缀即可.

在串指令操作中,si和ds联用,确定目标源地址,di和es(附加段寄存器)联用,确定传送的目的地址.说白了就是,分别寻址数据段和附加段.在串指令中,si和di具有自加和自减功能,

段寄存器

寄存器 解释 备注
CS(Code Segment) 代码段
DS(Data Segment) 数据段
SS(StackSegment) 堆栈段
ES(Extra Segment) 附加数据段
FS 附加数据段
GS 附加数据段

标志寄存器

状态寄存器eflags,没有指令能够直接操作这个寄存器,是CPU根据指令的执行结果,自己操作这个寄存器

20221001005102

条件标志寄存器 解释 备注
OF(OverFlow Flag) 溢出标志位 用来反应有符号数加减法运算所得结果是否溢出。运算超出当前运算位数所能表示的范围,则称为溢出,标志位被置为1,否则为0。
SF(Sign Flag) 符号标志位 用来反应运算结果是否为0。运算结果为负时置为1,否则为0。
ZF(Zero Flag) 零标志位 用来反应运算结果是否为 0。为零时置为1,否则为0。
AF(Auxilliary carryFlag) 辅助进位标志位 在字操作址,发生低字节向高字节进位或借位时该标志位被置为1,否则为0。
PF(Parity Flag) 奇偶标志位 用于反应结果中“1”的个数的奇偶性。如果“1”为偶数置为1,否则为0。
CF(Carry Flag) 进位标志位 运算结果的最高位产生了一个进位或错位,则该标志位置为1,否则为0。
控制标志寄存器 解释 备注
DF(Direction Flag) 方向标志位 用于串操作指令中,控制地址的变化方向。当DF为0时,存储器地址自动增加;当 DF为1时,存储器地址自动减少。
IF(Interupt Flag) 中断标志位 用于控制外部可屏蔽中断是否可以被处理器响应。
TF(Trap Flag) 陷阱标志位 用于控制处理器是否进入单步操作方式。当TF为0时,处理器在正常模式下运行;当为1时,处理器单步执行指令,调试器可以逐步指令进行执行就是使用了该标志位。

数据宽度

20221004161145

寄存器 宽度 类型
rax 64bit long
eax 32bit int
ax 16bit short
ah 8bit ax寄存器的高八位
al 8bit al寄存器的低八位

汇编指令

数据传输指令

寄存器传输 说明
MOV 传送字或字节.
MOVSX 先符号扩展,再传送.
MOVZX 先零扩展,再传送.
PUSH 把字压入堆栈.
POP 把字弹出堆栈.
PUSHA 把AX,CX,DX,BX,SP,BP,SI,DI依次压入堆栈.
POPA 把DI,SI,BP,SP,BX,DX,CX,AX依次弹出堆栈.
PUSHAD 把EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI依次压入堆栈.
POPAD 把EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX依次弹出堆栈.
BSWAP 交换32位寄存器里字节的顺序
XCHG 交换字或字节.(至少有一个操作数为寄存器,段寄存器不可作为操作数)
CMPXCHG 比较并交换操作数.(第二个操作数必须为累加器AL/AX/EAX)
XADD 先交换再累加.(结果在第一个操作数里)
XLAT 字节查表转换.----BX指向一张256字节的表的起点
AL为表的索引值(0-255,即0-FFH)
返回AL为查表结果.([BX+AL]->AL)
标志传输 说明
LAHF 标志寄存器传送,把标志装入AH.
SAHF 标志寄存器传送,把AH内容装入标志寄存器.
PUSHF 标志入栈.
POPF 标志出栈.
PUSHD 32位标志入栈.
POPD 32位标志出栈.

注意:操作内存,内存地址需要用中括号包起来,不然程序就不能区分是内存地址还是立即数

内存传输 说明
LEA 装入有效地址.例: LEA DX,string ;把偏移地址内容存到DX.
LDS 传送目标指针,把指针内容装入DS.例: LDS SI,string ;把段地址:偏移地址存到DS:SI.
LES 传送目标指针,把指针内容装入ES.例: LES DI,string ;把段地址:偏移地址存到ES:DI.
LFS 传送目标指针,把指针内容装入FS.例: LFS DI,string ;把段地址:偏移地址存到FS:DI.
LGS 传送目标指针,把指针内容装入GS.例: LGS DI,string ;把段地址:偏移地址存到GS:DI.
LSS 传送目标指针,把指针内容装入SS.例: LSS DI,string ;把段地址:偏移地址存到SS:DI.
内存传输其意义是同时给一个段寄存器和一个16位通用寄存器同时赋值
具体如下:reg16=mem32的低字,DS=mem32的高字,例如、

地址 100h 101h 102h 103h
内容 00h 41h 02h 03h

如果指令 LDS AX,[100h]
则结果为 AX=4100h DS=0302h
串传输 说明
MOVS 串传送.( MOVSB 传送字符. MOVSW 传送字. MOVSD 传送双字. )
CMPS 串比较.( CMPSB 比较字符. CMPSW 比较字. )
SCAS 串扫描.把AL或AX的内容与目标串作比较,比较结果反映在标志位.
LODS 装入串.把源串中的元素(字或字节)逐一装入AL或AX中.
LODSB 传送字符. LODSW 传送字. LODSD 传送双字. )
STOS 保存串.是LODS的逆过程.
REP 当CX/ECX<>0时重复.
REPE/REPZ 当ZF=1或比较结果相等,且CX/ECX<>0时重复.
REPNE/REPNZ 当ZF=0或比较结果不相等,且CX/ECX<>0时重复.
REPC 当CF=1且CX/ECX<>0时重复.
REPNC 当CF=0且CX/ECX<>0时重复.

输入输出指令

汇编语言中,CPU对外设的操作通过专门的端口读写指令来完成;

  读端口用IN指令,写端口用OUT指令。   例子如下:   IN AL,21H;表示从21H端口读取一字节数据到AL   IN AX,21H;表示从端口地址21H读取1字节数据到AL,从端口地址22H读取1字节到AH   MOV DX,379H   IN AL,DX ;从端口379H读取1字节到AL   OUT 21H,AL;将AL的值写入21H端口   OUT 21H,AX;将AX的值写入端口地址21H开始的连续两个字节。(port[21H]=AL,port[22h]=AH)   MOV DX,378H   OUT DX,AX ;将AH和AL分别写入端口379H和378H

运算指令

算数运算 说明
ADD 加法.
ADC 带进位加法.
INC 加 1.
AAA 加法的ASCII码调整.
DAA 加法的十进制调整.
SUB 减法.
SBB 带借位减法.
DEC 减 1.
NEG 求反(以 0 减之).
CMP 比较.(两操作数作减法,仅修改标志位,不回送结果).
AAS 减法的ASCII码调整.
DAS 减法的十进制调整.
MUL 无符号乘法.结果回送AH和AL(字节运算),或DX和AX(字运算),
IMUL 整数乘法.结果回送AH和AL(字节运算),或DX和AX(字运算),
AAM 乘法的ASCII码调整.
DIV 无符号除法.结果回送:商回送AL,余数回送AH, (字节运算);或 商回送AX,余数回送DX, (字运算).
IDIV 整数除法.结果回送:商回送AL,余数回送AH, (字节运算);或 商回送AX,余数回送DX, (字运算).
AAD 除法的ASCII码调整.
CBW 字节转换为字. (把AL中字节的符号扩展到AH中去)
CWD 字转换为双字. (把AX中的字的符号扩展到DX中去)
CWDE 字转换为双字. (把AX中的字符号扩展到EAX中去)
CDQ 双字扩展. (把EAX中的字的符号扩展到EDX中去)
DS:SI 源串段寄存器 :源串变址.
ES:DI 目标串段寄存器:目标串变址.
CX 重复次数计数器.
AL/AX 扫描值.
D标志 0表示重复操作中SI和DI应自动增量; 1表示应自动减量.
Z标志 用来控制扫描或比较操作的结束.
逻辑运算 说明
AND 与运算.
OR 或运算.
XOR 异或运算.
NOT 取反.
TEST 测试.(两操作数作与运算,仅修改标志位,不回送结果).
SHL 逻辑左移.
SAL 算术左移.(=SHL)
SHR 逻辑右移.
SAR 算术右移.(=SHR)
ROL 循环左移.
ROR 循环右移.
RCL 通过进位的循环左移.
RCR 通过进位的循环右移.

以上八种移位指令,其移位次数可达255次.移位一次时, 可直接用操作码. 如 SHL AX,1.移位>1次时, 则由寄存器CL给出移位次数.
如 MOV CL,04 SHL AX,CL

程序转移指令

任何语言的底层,循环结构及条件判断,都是基于cflags寄存器+JCC指令实现的

20221008215823

无条件转移指令 (长转移)
JMP 无条件转移指令
CALL 过程调用
RET/RETF 过程返回

条件转移指令 (短转移,-128到+127的距离内)( 当且仅当(SF XOR OF)=1时,OP1<OP2 )
JA/JNBE 不小于或不等于时转移.
JAE/JNB 大于或等于转移.
JB/JNAE 小于转移.
JBE/JNA 小于或等于转移.
以上四条,测试无符号整数运算的结果(标志C和Z).
JG/JNLE 大于转移.
JGE/JNL 大于或等于转移.
JL/JNGE 小于转移.
JLE/JNG 小于或等于转移.
以上四条,测试带符号整数运算的结果(标志S,O和Z).
JE/JZ 等于转移.
JNE/JNZ 不等于时转移.
JC 有进位时转移.
JNC 无进位时转移.
JNO 不溢出时转移.
JNP/JPO 奇偶性为奇数时转移.
JNS 符号位为 "0" 时转移.
JO 溢出转移.
JP/JPE 奇偶性为偶数时转移.
JS 符号位为 "1" 时转移.

循环控制指令(短转移),CX会自动减一
LOOP CX不为零时循环.
LOOPE/LOOPZ CX不为零且标志Z=1时循环.
LOOPNE/LOOPNZ CX不为零且标志Z=0时循环.
JCXZ CX为零时转移.
JECXZ ECX为零时转移.

标志处理指令
CLC 进位位置0指令
CMC 进位位求反指令
STC 进位位置为1指令
CLD 方向标志置1指令
STD 方向标志位置1指令
CLI 中断标志置0指令
STI 中断标志置1指令
NOP 无操作
HLT 停机
WAIT 等待
ESC 换码
LOCK 封锁

JA/JB用于无符号数,JG/GL用于有符号数

cmp本质上做减法运算,test本质上做与运算

cmp eax,0 等价于 sub eax,0 差别是cmp的运算结果只会改eflags寄存器,不会修改eax寄存器的值 通常配合JCC指令使用实现条件跳转

test本质上做与运算

test eax,0 等价于 and eax,0 差别是test的运算结果只会改eflags寄存器,不会修改eax寄存器的值 通常配合JCC指令使用实现条件跳转

处理器指令

中断指令
INT 中断指令
INTO 溢出中断
IRET 中断返回

处理器控制指令
HLT 处理器暂停, 直到出现中断或复位信号才继续.
WAIT 当芯片引线TEST为高电平时使CPU进入等待状态.
ESC 转换到外处理器.
LOCK 封锁总线.
NOP 空操作.
STC 置进位标志位.
CLC 清进位标志位.
CMC 进位标志取反.
STD 置方向标志位.
CLD 清方向标志位.
STI 置中断允许位.
CLI 清中断允许位.

masm和nasm

masm是微软专门为windows下汇编而写的,而nasm可以在windows、linux等系统下汇编

nasm 区分大小写,在 nasm 语法里,对 memory 操作数需要加 [ ] 括号,对于 绝对地址 形式,缺省是 32 位的,因此,需要明确使用 qword 来指明 64 位的 address size

伪指令不是 x86/x64 机器的真实指令,伪指令是用于给编译器指示如何进行编译。

dos程序返回

最后两条指令返还控制权给系统单任务程序

mov ax,4c00h
int 21h

Intel syntax vs AT&T syntax

  • 这是两种不同的汇编语法,可以简单地认为是两种不同的汇编语言
  • Intel syntax主要用于DOS和Windows,而AT&T syntax主要用于UNIX。
  • AT&T是American Telephone and Telegraph的缩写,AT&T是贝尔实验室的创建者之一,而UNIX系统在贝尔实验室诞生,因此UNIX下的汇编语言称为AT&T syntax。
  • GNU的汇编器(即下文中的GAS)采用AT&T syntax,如 gcc -S filename.c 会生成AT&T syntax风格的汇编代码文件filename.s,如果想要生成Intel syntax风格的汇编代码,可以使用 gcc -S -masm=intel filename.c 命令。
  • Intel syntax和AT&T syntax在编码上最大也是最应引起注意的区别是:两者指令的原操作数和目的操作数的位置正好是相反的。

GAS vs NASM

这是两种不同的汇编器,

  • GAS是GNU Assembler的简写,基于AT&T syntax指令,生成.s文件。
  • NASM是Netwide Assembler的简写,基于Intel syntax指令,生成.asm文件。
  • 还有其它汇编器,如MASM (Microsoft Macro Assembler)、FASM (Flat Assembler)、TASM (Turbo Assembler)、YASM (Yet Another Assembler)等。常见汇编器的对比如下图所示

masm组织伪指令

  1. 段定义语句

为了与存储器的分段结构相对应,所以汇编指令也提供了对应的段的组织方式

段名 SEGMENT [定位类型] [组合类型] [‘类别’]

段名 ENDS

;数据段
DSEG SEGMENT
MESS DB 'HELLO'
DSEG ENDS
;代码段
CSEG SEGMENT
  MOV AX, DSEG
    ...
    ...
CSEG ENDS

段使用设定语句 ASSUME 段寄存器名: 段名 [, 段寄存器名: 段名, 段寄存器名: 段名 ..]

设定了段之后汇编程序需要知道各自段对应是用来干嘛的,并设置对应的段寄存器ASSUME CS: CSEG,DS:DSEG

ASSUME 是伪指令,所以汇编编译器其实是将其转换成了对应的汇编指令,所以ASSUME可以出现在代码段的任何位置。随时进行切换

DSGE1 SEGMENT
   .....
DSGE1 ENDS

DSEG2 SEGMENT
   ......
DSGE2 ENDS

CSEG SEGMENT
ASSUME CS:CSEG DS:DSEG1 ES:DSEG2
MOV AX,DSEG1      ; 由于此时数据段就是DSEG1 所以指令就是直接翻译的
MOV AX,DSEG2      ; 由于此时数据段是DSEG1,所以实际语句会被翻译成 MOV AX, ES:DSEG2

汇编编译器在汇编的时候使用汇编地址计算器来计算每条指令的偏移地址,而ORG指令就是用于手动修改当前地址的

$表示当前指令的第一个字节的地址

org $+8 表示表示地址计算器从此处开始向后空8个字节出来

jmp $+ 6 转跳到本条指令之后6个字节处,注意计算地址是JMP指令的开始位置不是结束位置,所以这6个自己包含了JMP本身的长度

nasm伪指令

组织伪指令

SECTION是一种组织代码和存储的方式

  • NASM支持标准的.data, .text和.bss,编译后的程序文件中的内存地址顺序是.text, .data,用户自定义section。
  • NASM支持用户自定义section
  • 同名的section,编译后会放在同一块连续的内存上
  • 对用户自定义section,按照出现的先后顺序存储,同名的section存储在一起。
SECTION .data
var1 db 0x01
SECTION .text
MOV AX, var1
SECTION .data
var2 db 0x02

编译后,内存为0xB8040000 0102,其中0xB804是MOV AX,0x04的机器码,0x04是标号var1汇编后的偏移地址。因为汇编后,var1对应的存储区在.data段,被挪到了内存的尾部,因此偏移不是0x00,而变成了0x04。

每个SECTION默认都是按4字节对齐的:SECTION的对齐方式可以用ALIGN来调整

SECTION .s1
var1 db 0x1
SECTION .s2
var2 db 0x3
SECTION .s1
var3 db 0x2

编译后产生的内存:0x01020000 0x03 可以看到SECTION .s1被扩展为4个字节,后面两个字节填0,然后是SECTION .s2

$$指向当前section相对于段基址的偏移地址,$指向当前行相对于段基址的偏移地址。

初始化数据:db 家族

数据类型 大小 伪指令 含义
byte 8 db define byte
word 16 dw define word
dword 32 dd define doubleword
qword 64 dq define quadword
tword 80 dt define tword
oword 128 do define oword
yword 256 dy define yword
db    0x55                ; just the byte 0x55
db 0x55,0x56,0x57 ; three bytes in succession
db 'a',0x55 ; character constants are OK
db 'hello',13,10,'$' ; so are string constants
dw 0x1234 ; 0x34 0x12
dw 'a' ; 0x61 0x00 (it's just a number)
dw 'ab' ; 0x61 0x62 (character constant)
dw 'abc' ; 0x61 0x62 0x63 0x00 (string)
dd 0x12345678 ; 0x78 0x56 0x34 0x12
dd 1.234567e20 ; floating-point constant
dq 0x123456789abcdef0 ; eight byte constant
dq 1.234567e20 ; double-precision float
dt 1.234567e20 ; extended-precision float

非初始化数据:resb 家族

resb 相当于 Microsoft MASM 语法中的 db ?

伪指令 含义
resb reserve byte
resw reserve word
resd reserve doubword
resq reserve quadword
rest reserve tword
reso reserve oword
resy reserve yword
buffer:         resb    64              ; reserve 64 bytes
wordvar: resw 1 ; reserve a word
realarray resq 10 ; array of ten reals
ymmval: resy 1 ; one YMM register

包含 binary 文件

asm 提供了一种包含 binary(二进制)文件的方法:使用 incbin 伪指令。incbin 伪指令包含的 binary 文件将直将写入输出文件中。此伪指令的作用是包含 graphics 以及 sound 这类数据文件。

incbin  "file.dat"             ; include the whole file
incbin "file.dat",1024 ; skip the first 1024 bytes
incbin "file.dat",1024,512 ; skip the first 1024, and
; actually include at most 512

$标号和times重复

$ 标号表示 nasm 编译后当前指令位置

$$ 标号表示当前 section 起始位置

times 510-($-$$) db 0
dw 0xaa55
;这段代码经常出现在 boot 磁盘 MBR 引导代码中,目的是除了最后 2 个字节和 code 代码外的区域全部写 0 值。

section .rdata
dq 0

section .text

mov rax, 0
mov rax, $-$$

使用equ定义常量

equ 用来为标识符定义一个 整型 常量,它的作用类似 C 语言中的 #define

a  equ 0                          ; OK
b equ 'abcd' ; OK! b = 0x64636261
c equ 'abcdefghi' ; warning! c = 0x6867666564636261
d equ 1.2 ; error!


section .data

string db 'hello,word',0
len equ $-string ; OK! len = 0x0b

section .text
textlen equ _end - entry ; OK! textlen = 0x05

_entry:
mov ecx, textlen

_end:

例子中: b 定义为常量 'abcd' 它将是字符串的 ASCII 码序列,‘abcdefghi' 常量将会被截断,整型常量最长为 quadword(8 bytes),而 d 企图被定义为一个 float 常量,这产生会错误。len 和 textlen 被定义为编译期确定的数值。

调试工具

DOS程序加载过程

20221011020752

CX中存放了程序长度

windows调试

windows下使用dosbox学习调试

在vscode中直接安装MASM/TASM插件即可.右键就可以调试

DOSBox下debug命令

参数 含义
-g 执行到指定ip
-a 编写汇编命令
-t 单步执行
-p 直接执行完不是单步执行
-u 反编译
-r 查看修改寄存器的值
-d 查看内存单元
-e 修改内存单元
-? 查看指令帮助

vscode安装以下插件进行调试

20221008231726

data segment ;数据段
string db 'hellow world$'
data ends
code segment ;代码段
assume cs:code,ds:data
start:
mov ax,data ;获取段基址
mov ds,ax ;将段基址送入寄存器
mov dx,offset string
mov ah,9
int 21h
mov ah,4ch
int 21h
code ends
end start

linux调试

# 安装
sudo apt-get install nasm
# 编译
nasm -f elf -l hello.lst -g hello.asm
# 链接
ld -o hello hello.o
nasm参数 说明
-f elf是表示生产elf格式的目标文件,elf32,elf64
-g 是生产调试信息到目标文件
-l hello.lst对应的是指令和数据在段中偏移量,不要这个也可以

ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000

原因:链接器在做程序链接的时候没有找到 _start 这个符号。(_start 是 arm 汇编程序的入口)

解决方法:在 _start 前面加上声明 . global _start

  section .data
msg:
db "hello, world", 10
len equ $-msg

section .text
global main
main:
mov edx, len
mov ecx, msg
mov ebx, 1
mov eax, 4 ;直接使用sys_write系统调用
int 0x80

mov ebx, 0
mov eax, 1
int 0x80

注意在源代码中加:global main main:,因为程序的入口函数是main,就像c中我们要写个main函数一样,gcc连接器在连接的时候就是找这个main标号,其实在目标代码中它就是一个符号名。

和调试c语言一样,直接用gdb hello命令进入调试。

具体调试指令可以参考gdb指令gdb汇编