目录
-
- 安装环境
- 第一章 基本概念
- 第二章 x86处理器架构
- 第三章 汇编语言基础
- 第四章
- 第五章 过程
-
- 第六章 条件处理
- 第七章 整数运算
- 第八章 高级过程
安装环境
- 本书源代码 http://asmirvine.com/gettingStartedVS2017/Irvine.zip
- https://blog.csdn.net/caipengbenren/article/details/88148018
brew install --cask dosbox
mkdir -p ~/dos/masm
# 把当前文件下的文件夹masm的所有文件复制到~/dos/masm中,这样就成功安装masm
# 打开dosbox
mount c ~/dos/masm # 挂载本地磁盘
# hello.asm 已在masm中
masm hello.asm # 一直回车到ok
link HELLO.OBJ # 一直回车到ok
hello.exe # 运行输出
brew reinstall nasm
nasm -f macho64 -o hello.o hello64.asm # 64
ld -o hello -e _main hello.o -static
第一章 基本概念
- 汇编器是一种程序,用于将源程序从汇编语言转换为机器语言。
- 链接器是把汇编器生成的单个文件组合成一个可执行程序。
- 调试器:为程序员提供一种途径来追踪程序的执行过程,并查看内存的内容。
- 汇编语言和机器语言是一对一的关系,即一条汇编指令对应于一条机器指令,汇编语言不具有移植性,它与具体的处理器绑定。
- 在展示计算机体系结构中的每一层如何表示为一个机器抽象时,虚拟机概念是一种有效的方式。每层可以用硬件或软件构成,其上编写的程序可以用其下一层进行翻译或解释。虚拟机概念可以与真实世界中的计算机层次相关,包括数学逻辑、指令集架构、汇编语言和高级语言。
第二章 x86处理器架构
- CPU是进行算术和逻辑操作的部件,包含了有限数量的存储位置——寄存器(register),一个高频时钟、一个控制单元和一个算术逻辑单元。
- 时钟对CPU内部操作与系统其他组件进行同步
- 控制单元协调参与机器指令执行的步骤序列
- 算术逻辑单元执行算术运算,如加法、减法,以及逻辑运算
- 总线bus是一组并行线,用于将数据从计算机的一部分传送到另一部分
- 数据总线:在CPU和内存之间传送指令和数据
- I/O总线:在CPU和系统输入/输出设备之间传输数据
- 控制总线:用二进制信号对所有连接在系统总线上设备的行为进行同步
- 地址总线:
当前执行指令在CPU和内存之间传输地址时,地址总线用于保持指令和数据的地址。
- 指令执行周期:取值Fetch、译码Decode和执行Execute
- CPU从内存读取一个值的步骤:
- 将想要读取的值的地址放到地址总线上
- 设置处理器RD(读取)引脚(改变RD的值)
- 等待一个时钟周期给存储器芯片进行响应
- 将数据总数据总线复制到目标操作数。
- 程序加载器:加载程序到内存。
- x86处理器操作模式
- 保护模式:处理器原生状态,所有的指令和特性都是可用的。分配给程序的独立内存区域称为段,而处理器会阻止程序使用自身段范围之外的内存。
- 虚拟8086模式:保护模式下,处理器可以在一个安全环境中直接执行实地址模式软件,如MS-DOS程序。
- 实地址模式:早期的Intel处理器编程环境,但是增加了一些其他的特性,如切换到其他模式的功能。当程序需要直接访问系统内存和硬件设备时,这个模式就很有用。
- 系统管理模式:向操作系统提供了实现诸如电源管理和系统安全等功能的限制
- 基本程序执行寄存器
- 通用寄存器:主要用于算术运算、数据传输和逻辑操作
- EAX EBX ECX EDX。EAX ,16位 AX, 8位AH和AL
- ESI EBP EDI ESP。只有16位 SI等,没有8位
- 特殊用法
- 乘法指令默认使用EAX,它常常被称为扩展累加器寄存器(extended accumulator)
- CPU默认使用ECX为循环计数器
- ESP用于寻址堆栈数据。它极少用于一般算术运算和数据传输,被称为扩展堆栈指针寄存器(extended stack register)。
- ESI和EDI用于高速存储器传输指令,也被称为扩展源变址寄存器(extended source register)和扩展目的变址寄存器(extended destination register)。
- 高级语言通过EBP来引用堆栈中的函数参数和局部变量。一般不用于算术运算和数据传输。也被称为扩展帧寄存器(extended frame register)
- 段寄存器:16位段寄存器用于表示的是预先分配的内存区域的基址,这个内存区域称为段。保护模式中,段寄存器中存放的是段描述符表指针。
- 指令指针:EIP寄存器中包含下一条将要执行指令的地址。某些机器指令能控制EIP,使得程序分支转向一个新位置
- EFLAGS寄存器:包含一个独立的二进制位,用于控制CPU的操作,或者反映一些CPU操作的结果。有些指令可以测试和控制这些单独的处理器标志位
- 控制标志位:控制CPU的操作
- 状态标志位:反映了CPU执行的算术和逻辑操作的结果。
- 进位标志位(CF):无符号算术运算结果太大时,设置该标志位
- 溢出标志位(OF):有符号算术运算太大或太小时,设置该标志位
- 符号标志位(SF):算术或逻辑操作产生负结果时,设置该标志位
- 零标志位(ZF):算术或逻辑操作产生结果为零时,设置该标志位
- 辅助进位标志位(AC):算术操作在8位操作数中产生了位3向位4进位时,设置该标志位
- 奇偶校验标志位(PF):结果的最低有效字节包含偶数个1时,设置该标志位,否则,清除该标志位。用于错误检测。
- MMX寄存器:8个64位MMX寄存器支持称为SIMD的特殊指令。提升Intel的处理器性能
- XMM寄存器:8个128位XMM寄存器,被用于SIMD流欧战指令集
- 内存管理
- 保护模式是最可靠、最强大的,但是她对应用程序直接访问系统硬件有着严格的限制
- 在实地址模式,只能寻址1MB内存。地址从00000H到FFFFFH。处理器一次只能运行一个程序,但是可以暂时中断程序来处理来自外围的请求(中断)。
第三章 汇编语言基础
.data
sum DWORD 0 ; 定义名为sum的变量
- 保留字:有特殊意义并且只能在其正确的上下文中使用
- 标识符:程序员选择的名称,它用于标识变量、常数、子程序和代码标签。
- 伪指令(directive)是嵌入源代码中的命令,由汇编器识别和执行
- DWORD伪指令告诉汇编器在程序中为一个双字节变量保存空间
myVar DWORD 26
mov eax,myVar
- .data:标识一个段可用于定义变量
- .code:标识程序区段包含了可执行的指令
- 指令:在程序汇编编译时变得可执行。[label:] mnemonic [operands] [;comment]
- 标号:指令和数据的位置标记
- 指令助记符:mov add等
- 操作数:个数范围为1~3个。
stc ; 进位标志位置1
inc eax ; eax加1
mov count,ebx ; 将abx传送给变量count
imul eax,ebx,5 ;将ebx与5相乘,结果存放到eax中
- 注释:单行注释 “;”。块注释
COMMENT !
this is comment
this is comment
!
BYTE 20 DUP(0) ; 20个字节,值都为0
BYTE 20 DUP(?) ; 20个字节,非初始化
BYTE 4 DUP("STACK") ; 20个字节
-
汇编器读取源文件,并生成目标文件,即对程序的机器语言翻译
-
链接器读取并检查目标文件,以便发现该程序是否包含了任何对链接库中过程的调用。链接器从链接库中复制任何被请求的过程,将它们与目标文件组合,以生成可执行文件。
-
列表文件(listing file)包括了程序源文件的副本,再加上行号、每条指令的数字地址、每条指令的机器代码字节以及符号表。
-
符号表中包含了程序中所有标识符的名称、段和相关信息。
-
符号常量:为整数表达式或文本指定标识符创建符号常量。不预留存储空间,它只在汇编器扫描程序时运行。当前地址计数器符号 $
- 等号伪指令:把一个符号名称与一个整数连接起来
- EQU伪指令:把一个符号名称与一个整数或任意文本连接起来
- TEXTEQU:文本宏。
COUNT = 500
mov eax,COUNT
selfPtr DWORD $ ; 声明变量,初始化为该变量的偏移量
list BYTE 10,20,30,40
ListSize = ($ - list)
PI EQU <3.14159>
第四章
- visual studio https://blog.csdn.net/weixin_43272781/article/details/104988520
- 操作数
- 立即数:使用数字文本表达式
- 寄存器操作数:使用CPU内已命名的寄存器
- 内存操作数:引用内存位置
- movzx进行全零扩展并传送:将源操作数复制到目的操作数,并把目的操作数0扩展到16位或32位。这条指令只用于无符号数。
.data
byteVal BYTE 10001111b
.code
movzx ax,byteVal ; ax=000000000 10001111b
- movsx进行符号扩展并传送。扩展到16位或32位,只用于有整数
.data
byteVal BYTE 10001111b
.code
movsx ax,byteVal ; ax=11111111 010001111b
- LAHF 加载状态标志到AH:将EFLAGS寄存器的低字节复制到AH
- SAHF:保存AH内容到状态标志位
- XCHG:交换指令,交换两个操作数内容
XCHG reg,reg
XCHG reg,mem
XCHG mem,reg
arrayC WORD 1234h,2345h,3456h,5678h
mov ax,[arrayC+2] ; AX=2345h
mov bx,[arrayC+4] ; BX=3456h
- CPU标志位
- UP 方向 EI 中断
- CY 进位,无符号整数溢出
- OV 溢出 有符号整数溢出
- ZR 零 意味着操作结果为0
- PL 符号 意味着产生的结果为负数
- PE 奇偶 在一条算术或布尔运算指令执行之后。立即判断目的操作数最低有效字节中1的个数是否为偶数
- AC 辅助进位 1表示目的操作数最低有效字节中位3有进位
- 加法和减法
- INC 自增 INC reg/mem
- DEC 自减
- ADD 将长度相同的源操作数和目的操作数进行相加操作
- SUB desc,source 从目的操作数中减去源操作数
- NEG 非指令,把操作数转换为二进制补码
- 与数据相关的运算符和伪指令
- OFFSET 运算符返回的是一个变量与其所在段起始地址之间的距离 mov esi,OFFSET aVal
- ALIGN伪指令将一个变量对其。ALIGN bound(1,2,4,8,16)
- PTR 运算符可以重写操作符默认的大小类型。可大送小,也可小送大
.data
myDouble DWORD 12345678h
wordList WORD 5678h,1234h
.code
mov ax,WORD PTR myDouble ; 5678h
mov bx,WORD PTR [myDouble+2] ; 1234h
; x86是小端序,低地址存放高位。78 56 34 12
; 将较小的值送入较大的目的操作数
mov eax,DWORD PTR wordList ;EAX=12345678h
- TYPE运算符 返回变量单个元素的大小
- LENGTHOF运算符 计算数组中元素的个数
- SIZEOF运算符 = LENGTHOF * TYPE
- LABEL伪指令可以插入一个符号,并定义它的大小属性,但是不为这个符号分配存储空间。LABEL常见用法是,为数据段中定义的下一个变量提供不同的名称和大小属性
.data
val16 LABEL WORD
val32 DWORD 12345678h
.code
MOV ax,[val16] ; 5678h
MOV DX,[val16+2] ; 1234h
- 间接寻址:寄存器作为指针
- 任何一个32位通用寄存器(EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP)加上括号就能构成一个间接操作数。寄存器中存放的是数据的地址。
mov eax,OFFSET myDouble
mov ebx,[eax] ; 把myDouble的值写入ebx寄存器中
mov ecx,888888
mov [eax],ecx ; ecx内容写入寄存器eax存放值的内存地址
.data
arrayD DWORD 1,2,3,4
arrayB BYTE 1,2,3,4
mov esi,0
mov al,arrayB[esi] ; arrayB[esi] = [arrayB+esi] al=1
mov eax,arrayD[esi*4] ;eax=4
mov eax,arrayD[esi*TYPE arrayD] ;eax=4
- 指针:如果一个变量包含另一个变量的地址,则该变量称为指针。
.data
arrayB BYTE 10h,20h,30h,40h
ptrB DWORD arrayB ; ptrB包含了arrayB的偏移量
ptrB DWORD OFFSET arrayB
- TYPEDEF运算符:可以创建用户自定义类型,这些类型包含了定义变量时内置类型的所有状态。
PBYTE TYPEDEF PTR BYTE ;字节指针
PWORD TYPEDEF PTR WORD ;字指针
PDWORD TYPEDEF PTR DWORD ; 双字指针
- JMP和LOOP指令
- 无条件转移:无论什么情况都会转移到新地址。JMP destination
- 条件转移:满足某种条件,则程序出现分支。CPU基于ECX和标志寄存器的内容来解释真/假条件。
- LOOP指令:正式称为按照ECX计数器循环,将程序块重复特定次数。ECX自动成为计数器,每循环一次计数值减1。
.data
count DWORD ?
.code
main proc
mov eax,0h
mov edx,0h
mov ecx,100h
L1:
inc eax
mov count,ecx ; 外层L1循环次数保存变量
mov ecx,20h
L2: ; 嵌套循环
inc edx
loop L2
mov ecx,count ; 外层循环次数还原
loop L1
第五章 过程
- 堆栈的应用
- 当寄存器用于多个目的时,堆栈可以作为寄存器的一个方便的临时保存区。在寄存器被修改后,还可以恢复其初始值。
- 执行CALL指令时,CPU在堆栈中保存当前过程的返回地址。
- 调用过程时,输入数值也被称为参数,通过将其压入堆栈实现参数传递。
- 堆栈也为过程局部变量提供了临时存储区域。
- PUSH POP
PUSH reg/mem16
PUSH reg/mem32
POP reg/mem16
POP reg/mem32
- PUSHFD POPFD:把32位的EFLAGS寄存器压入栈或出栈
- PUSHAD指令按照EAX ECX EDX EBX ESP(执行PUSHAD之前的值)、EBP ESI EDI的顺序入栈,POPAD按相反顺序出栈
定义并使用过程
- 过程用PROC和ENDP伪指令定义。
- 在程序启动过程之外创建一个过程时,就用RET指令来结束它,RET强制CPU返回到该过程被调用的位置
- CALL指令调用一个过程,指挥处理器从新的内存地址开始执行。从物理上来说,CALL指令将其返回地址压入堆栈,再把调用过程的地址复制到指令指针寄存器。当过程准备返回时,它的RET指令从堆栈把返回地址弹回到指令指针寄存器。32位模式下,CPU执行的指令从EIP(指令指针寄存器)在内存中指出。
- 向过程传递寄存器参数
.data
theSum DWORD ?
.code
main PROC
MOV eax,1000h ;参数
mov ebx,2000h ;参数
mov ecx,3000h ;参数
call SumOf ;EAX=EAX+EBX+ECX
mov theSum,eax ;保存和数
...
- 保存和恢复寄存器值
- USES运算符,让程序员列出过程中修改的所有寄存器名,USES告诉汇编器:
- 在过程开始时生成PUSH指令,将寄存器保存到堆栈;
- 在过程结束时生成POP指令,从堆栈恢复寄存器的值
; USES esi ecx 相当于注释的push和pop
ArraySum proc USES esi ecx
;push esi ; save ESI, ECX
;push ecx
mov eax,0 ; set the sum to zero
L1:
add eax,[esi] ; add each integer to sum
add esi,TYPE DWORD ; point to next integer
loop L1 ; repeat for array size
;pop ecx ; restore ECX, ESI
;pop esi
ret ; sum is in EAX
ArraySum endp
当过程利用寄存器(通常时EAX)返回数值时,保存使用寄存器的惯例就出现了一个例外。在这种情况下,返回寄存器不能被压入和弹出堆栈。
链接到外部库
- 链接库是一种文件,包含了已经汇编为机器代码的过程(子程序)。链接库开始时一个或多个源文件,这些文件再被汇编成目标文件。目标文件插入到一个特殊格式文件,该文件由链接器工具识别。
- 假设程序要调用过程WriteString在控制台窗口显示一个字符串。该过程源代码必须包含PROTO伪指令来标识WriteString过程。
- 当程序进行汇编时,汇编器不指定CALL指令的目标地址,它知道这个地址将由链接器指定。链接器在链接库中寻找WriteString,并把库中适当的机器指令复制到程序的可执行文件中。同时,它把WriteString的地址插入到CALL指令。如果被调用过程不在链接库中,链接器就发出错误信息。且不会生成可执行文件。
第六章 条件处理
- TEST指令 不修改操作数的and比较
- AND指令 OR NOT XOR
- CMP 比较整数;执行从目标操作数中减去源操作数的隐含减法操作,并且不修改任何操作。
- 条件跳转
- JZ/JNZ 为零跳转/非零跳转
- JC/JNC 进位跳转/无进位跳转
- JO/JNO 溢出跳转
- JS/JNS 符号跳转
- JP/JNP 偶校验/奇校验跳转
- 相等性比较
- 基于无符号比较
- jA 大于跳转 JAE 大于等于
- JB 小于跳转 JBE
- JNA 不大于跳转
- JNB 不小于跳转
- 有符号比较
- JG 大于跳转 JGE 大于等于
- JL 小于跳转 JLE
- JNL 不小于跳转
- JNG 不大于跳转
- 条件循环指令
- LOOPZ和LOOPE 为零跳转 相等跳转
- LOOPNZ LOOPNE
- 条件结构
if (op1>op2){x=1;y=2;}else{z=3;}
mov eax,op1
cmp eax,op2
jAE L1
mov x,1
mov y,1
jmp L2
L1: mov z,3
L2:
- 表驱动选择:用查表来代替多路选择结构的一种方法。
- 有限状态机 FSM
第七章 整数运算
- LSB最低有效位 ;MSB最高有效位
- 逻辑移位,空出来的位用0填充。SHL SHR
- 算数移位,空出来的位用原数据的符号位填充。SAL SAR
- 位元循环:以循环方式来移位
- ROL 循环左移。把所有位都向左移
- ROL 循环右移
- 位组交换:利用ROL卡伊交换一个字节的高四位和第四位 rol al,4
- 带进位位元循环
- RCL带进位循环左移:把每一位都向左移,进位标志位复制到LSB,而MSB复制到进位标志位
- RCR 带进位循左移:
mov bl,88h ; CF,BL=0 10001000b
rcl bl,1 ; CF,BL=1 00010000b
rcl bl,1 ; CF,BL=0 00100001b
- 乘法
IMUL reg/mem8 ; AX = AL * reg/mem8
IMUL reg/mem16 ; DX:AX = AX * reg/mem16
IMUL reg/mem32 ; EDX:EAX = EAX * reg/mem32
- 除法
DIV reg/mem8 ; 被除数AX 商AL 余数AH
DIV reg/mem16 ; 被除数DX:AX 商 AX 余数 DX
DIV reg/mem32 ; 被除数EDX:EAX 商 EAX 余数 EDX
- CBW(字节转字)指令把AL的符号位扩展到AH寄存器。CDQ(双字转四字)指令把EAX的符号位扩展到EDX寄存器。CWD(字转双字)指令把AX的符号位扩展到DX寄存器。
- 扩展精度加减法:基本没有大小限制的加减法
- ADC(带进位加法)将源操作数和进位标志位的值都与目的操作数相加。
mov dl,0
mov al,0ffh
add al,0ffh ; AL = feh
adc dl,0 ; DL/AL = 01FEh ==> {DL 0000001(1) AL 11111110(FEh)}
- SBB(带借位减法):从目的操作数中减去源操作数和进位标志位的值
第八章 高级过程
- 大多数现代编程语言在调用子程序之前都会把参数压入堆栈。反过来,子程序也常常把它们的局部变量压入堆栈。
- 堆栈帧(或活动记录):是一块堆栈保留区域,用于存放被传递的实际参数、子程序的返回值、局部变量以及被保存的寄存器。创建步骤如下
- 被传递的实际参数。如果有,压入堆栈
- 当子程序被调用时,使该子程序的返回值压入堆栈。
- 子程序开始执行时,EBP被压入栈
设置EBP等于ESP。从这时开始,EBP就变成了该子程序所有参数的引用基址。
- 如果有局部变量,修改ESP以便在堆栈中为这些变量预留空间
- 如果需要保存寄存器,就将它们压入堆栈
- 寄存器参数:采用寄存器传递参数,调用子程序前需要把寄存器入栈,子程序返回需要出栈恢复寄存器。缺点,额外的出栈入栈不仅使代码混乱,还有可能消除性能优势。
- 值传递:当一个参数通过数值传递时,该值的副本会被压入堆栈。
.data
val1 DWORD 5
val2 DWORD 6
.code
push val2
push val1
call AddTwo
- 引用传递:通过引用来传递的参数包含的是对象的地址(偏移量)
push OFFSET val2
push OFFSET val1
call Swap
- 基址——偏移量寻址:访问堆栈,EBP是基址寄存器,偏移量是常数。
int AddTwo(int x,int y)
{
return x+y;
}
AddTwo PROC
push ebp
mov ebp,esp ; 堆栈帧的基址
mov eax,[ebp+12] ; 第二个参数
add eax,[ebp+8] ; 第一个参数
pop ebp
ret
AddTwo PROC
- 调用规范
- C调用规范:子程序的参数逆序入栈。程序调用子程序时,在CALL指令的后面紧跟一条语句使堆栈指针(ESP)加上一个数,该数的值即为子程序参数锁占用堆栈空间的总和。
; AddTwo(5,6)
Example1 PROC
push 6
push 5
call AddTwo
add esp,8 ; 从堆栈移除参数
ret
Example1 ENDP
- STDCALL 调用规范:给RET指令添加一个整数参数param,使得程序在返回到调用过程时,ESP会加上数值param。
AddTwo PROC
push ebp
mov ebp,esp ; 堆栈帧的基址
mov eax,[ebp+12] ; 第二个参数
add eax,[ebp+8] ; 第一个参数
pop ebp
ret 8 ; 清除堆栈
AddTwo PROC
- STDCALL不仅减少了子程序调用产生的代码量(减少了一条指令),还保证了调用程序永远不会忘记清除堆栈。
- C语言调用规范则允许子程序声明不同数量的参数,主调程序可以决定传递多少个参数
- 通常,子程序在修改寄存器之前要将它们的当前值保存到堆栈。
- 局部变量:高级语言中,在单一子程序内新建、使用和撤销的变量被称为局部变量。局部变量创建于运行时堆栈,通常位于基址指针(EBP)之下。尽管不能在汇编时给它们分配默认值,但是能在运行时初始化它们
void MySub(){
int X = 10;
int Y = 20;
}
x_local EQU DWORD PTR [ebp-4] ; 局部变量符号
y_local EQU DWORD PTR [ebp-8]
MySub PROC
push ebp
mov ebp,esp
sub esp,8 ; 创建局部变量
mov x_local,10 ; X
mov y_local,20 ; Y
mov esp,ebp ; 从堆栈在删除局部变量
pop ebp
ret
MySub ENDP
- 引用参数通常是由过程用基址——偏移量寻址(从EBP)方式进行访问
- LEA指令返回间接操作数地址。lea esi,[ebp-30]
- ENTER和LEAVE指令。ENTER指令为被调用的过程自动创建堆栈帧。LEAVE指令结束一个堆栈帧。
MySub PROC
ENTER 8,0
mov x_local,10 ; X
mov y_local,20 ; Y
LEAVE
ret
MySub ENDP
MySub PROC
LOCAL x_local:DWORD,y_local:DWORD
mov x_local,10 ; X
mov y_local,20 ; Y
ret
MySub ENDP
- 递归子程序:用局部变量在堆栈中保存数据
- INVOKE伪指令,只用于32位模式,将参数入栈并调用过程。ADDR运算符,在INVOKE调用过程中,它可以传递指针参数。
INVOKE FillArray,ADDR myArray, LENGTHOF myArray
- PROC
- PROTO伪指令为现有过程创建了原型(prototype)。原型声明了过程的名称和参数列表。它允许在定义过程之前对其进行调用,并验证参数的数量和类型是否与过程定义相匹配。