< 个人复习使用 >
汇编基础
汇编基础
-
可移植性
汇编语言是不可移植的,不同的处理器有着各自对应的汇编版本,常见的处理器有:Inter-IA32、IBM-370、SUN Sparc等。汇编指令可能与计算机体系机构相对应,或在执行期间由处理器内部的一段程序进行翻译(处理器内部翻译程序称为微代码解释器)。 -
语言的结构层次
微结构(第一层):操作计算机芯片的微指令,从内存取数字并加1可能需要几条微指令;
指令集体系系统(第二层):固化在处理器内部的指令集,用来执行加、减、移动等基本操作,这套指令成常规机器语言,每条机器语言可能需要分解成多条微指令;
操作系统(第三层):封装了上一层,理解并执行用户的指令,例如显示目录等,即我们通常所说的操作系统;
汇编语言(第四层):在操作系统层次之上,视为开发的一些大型程序翻译层,提供了一些很容易翻译成指令集的助记符(ADD、SUB、MOV…),其他一些汇编语句(如中断调用),则直接由操作系统执行;汇编语言在执行前需要一个翻译成二进制可执行程序(汇编)的过程;
高级语言(第五层):更加强大的语言功能,更容易被用户使用; -
微机的基本结构
CPU(中央处理器):包含多个寄存器、一个时钟、一个控制单元CU、一个算数逻辑单元ALU;CPU通过插入主板的引脚连接着数据总线、地址总线、控制总线
其他设备:内存设备、输入输出设备 -
指令的周期
程序在执行前需要装入内存,指令指针指向下一条需要执行的地址,指令队列包含了若干条需要执行的指令;
指令的执行步骤:取指令 -> 解码 -> 执行,若指令中用到了内存数据还需要从内存取数据和存入内存两个步骤;
<取指令>
CU单元从指令队列中取指令,并递增指令指针(IP,程序计数器)
<解码>
<执行>
ALU算数逻辑单元执行指令,并设置处理器状态标志 -
多任务调度
单cpu时执行多任务调度采用的是时间片原理,在某个时间片(操作系统任务调度程序为某个运行程序分配的一小段cpu运行时间)内,cpu执行一系列的指令; -
操作系统的模式
- 保护模式
处理器的基本模式,所有的特性和指令都是可用的,不可以使用操作系统分配段之外的内存。
在保护模式下,可运行高达4G内存(32位); - 实地模式
实地模式下只能访问1MB(20位)内存;
数据传输总线是16位,采用分段的形式,每段为64kb(16位),共分为0-F(4位)段; - 系统管理模式
- 保护模式
-
基本的寄存器
- 8个基本寄存器
EAX(累加和寄存器)、EBX(基地址寄存器)、ECX(循环计数寄存器)、EDX(数据寄存器)、
EBP(栈基指针寄存器)、ESP(堆栈指针寄存器,保存栈顶位置)、ESI(源地址寄存器)、EDI(目的地址寄存器) - 6个段寄存器(用作段+偏移量的内存访问模式)
CS(代码段)、ES(附加段)、SS(堆栈段)、FS(附加段)、DS(数据段)、GS(附加段) - 处理器状态寄存器
EFLAGS - 指令指针寄存器
EIP
- 8个基本寄存器
-
基本输入输出的过程调用
应用程序 -> 库函数 -> OS系统函数 -> BIOS功能函数 -> 硬件
汇编语法基础
数据类型
- 整数常量
格式:[ {+|-} ] 数字 [ 基数 ]
注:{}表示从多个中取一个,[]表示从多个中取一个或不取
基数(大小写均可)
r 编码实数
b/y 二进制
o/q 八进制
d/t 十进制
h 十六进制
-
字符(串)常量
字符(串)常量通常是以引号或者双引号括起来的一串字符; -
符号常量(类比宏定义)
使用伪指令=号赋值,例如:
COUNT=500;
符号常量仅将整数表达式和文本联系起来,符号常量不占用任何空间,符号常量仅在编译期使用;
数据定义
数据定义格式:数据标号 数据大小伪指令 数据内容(多个值用逗号隔开,字符串例外)
无论数据的内容是什么格式,最终都将转化为二进制保存;
数据内存可以为多个,表示连续内存保存,类比数组,list BYTE 10,32,41h,‘A’;如果10的地址是0000,32的地址则为0001;
保留字
(1)指令助记符(MOV、ADD…)
(2)伪指令
内嵌在程序源代码中,由汇编器识别并执行相应动作的命令。伪指令在程序运行时并不执行,可用于定义变量(例如DWORD:var DWORD 26)、宏及过程,可命名段以及执行许多其他与汇编器相关的簿记任务;如.data标识了程序中包含变量的区域,.code标识了包含指令的区域,.stack标识了运行栈的区域并指定运行栈的大小(.stack 100h)等等;
(3)指令
指令结构:[标号] 指令助记符 操作数 [注释]
标号(声明):可选,充当指令或者数据位置的标记
数据标号,array DWORD 1024,2048;标号array标记了数据1024的位置,2048在内存中紧跟其后
代码标号,target:mov ax,bx,通常用作循环和跳转目标位置
指令助记符(关键字);
操作数:
一条汇编指令可以操作0~3个操作数,每个操作数可以是常量表达式、寄存器、内存操作数(变量名或存了内存的寄存器)或IO端口;
在存在两个操作数的指令中,第一个操作数被称为目的操作数,第二个被称为源操作数;
注释:单行注释汇编语言会忽略单行;号后面所有的内容,多行注释COMMENT伪指令加用于自定义符号;
(4)属性
(5)运算符
(6)预定义符号
程序结构
段:程序是以段组织的,常见的段有代码段、堆栈段、数据段;代码段包含了一个或多个过程,其中一个是启动过程;堆栈段包含了过程需要的参数和局部变量;数据段包含了变量;
常用的指令
- NOP
空指令,不做任何事,占用一个字节; - INCLUDE(伪)
引入包含内容; - PROC / ENDP / END(伪)
过程的开始和结束;END标识为整个程序的最后一行(后面的内容都忽略),其后跟的标识符为程序入口点; - RET
过程返回(从ESP所指地址返回,若后面跟了常量参数,则返回后ESP + const value,栈指针指向压入参数前的位置); - INVOKE(伪)
调用过程或函数的伪指令; - PROTO(伪)
声明过程原型,在call 过程前必须有该声明; - DUP(伪)
使用计数器为多个数据项分配空间,BYTE 20 DUP(0),分配20个字节,值为零的内存; - .data / .data?
定义初始化和未初始化数据段伪指令; - $
获取当前地址偏移; - EQU
生成符号常量伪指令,name EQU < text > / name EQU expression(整数表达式),与直接使用=相比,他有一个不允许重复定义和允许使用文本赋值的优势; - TYPE
获取变量的类型大小; - OFFSET
返回变量相对于所在段开始的偏移; - LENGTHOF
返回数组内元素的数目; - PTR
重载操作数声明的默认尺寸;BYTE PTR var;
移位指令
- MOV des,src
mov指令规则:数据尺寸一样、不能同为内存操作数、立即数不能直接送到段寄存器、目的操作数不能为CS/EIP/IP - LAHF/SAHF var1,var2
LAHF将EFLAGS寄存器的低字节复制到AH寄存器中;SAHF将复制AH寄存器的值到EFLAGS寄存器中; - XCHG var1,var2
交换两个操作数的值,不接受立即操作数,其他和MOV指令有类似的规则;
算数指令
- INC reg/mem
操作数(寄存器或内存操作数)自增指令 - DEC reg/mem
操作数(寄存器或内存操作数)自减指令 - ADD des ,src
将同尺寸的源操作数和目的操作数相加,结果保存到目的操作数,格式和mov相同;
影响的标志:进位标志、零标志、符号标志、溢出标志、辅助进位标志和奇偶标志; - SUB des,src
将源操作数从目的操作数中减掉,格式和mov、add相同;
影响的标志位同add; - NEG reg/mem
将数字转换为对应的补码(相反数);
跳转指令
-
JMP dst
JMP指令导致向代码段内的目的地址做无条件转移;通常情况下,JMP指令只能跳转到当前过程内的标号处; -
LOOP dst
LOOP指令重复执行一块语句,执行的次数是特定的,ECX被自动做循环计数器,每次循环减一;
LOOP执行的步骤:1.ECX减一;2.与零比较,若符合条件则跳转到标号处执行;
堆栈指令
- PUSH data
首先减少ESP的值,再把一个16位或32位的源操作数复制到堆栈上; - POP data
首先将堆栈顶部元素复制到目的操作数,再增加ESP的值; - PUSHFD/POPFD
压入和弹出FLAGS的值 - PUSHAD/POPAD
按一定的顺序压入所有寄存器的值;按相反的顺序弹出所有寄存器的值;
数据操作
操作数分类
立即操作数:指令要操作的数据以常量的形式出现在指令中,称为立即数,它只能作为源操作数;例如0x10000h。
寄存器操作数:指令要操作的数据存放在CPU中的寄存器里,指令中给出寄存器名即可;例如mov eax, 0x1000h,eax为寄存器操作数。
内存操作数:指令要操作的数据存放在内存某些单元中,指令中给出内存单元物理地址(实际上指令只给出了偏移地址,段地址采用隐含方式给出,也可以使用跨段方式指出当前段地址);例如var BYTE 10h,变量名var表示引用的是内存中用来保存10h的地址,为内存操作数。
寻址
立即数寻址:mov eax , 0x10h
<内存寻址>
直接寻址:mov eax, [0x10000h]
<寄存器寻址>
寄存器间接寻址:寄存器保存内存地址,通过寄存器间接使用内存中的值 mov reg1,[reg2];可以配合PTR来明确数据的大小,例如寄存器中保存的是一个字的地址,使其自增:inc WORD PTR [reg2i];
寄存器相对寻址:寄存器esi保存基址地址,off表示偏移常量,通过使用基址寄存器加偏移量来获取数据,mov eax,[ esi + off ];
寄存器变址寻址:寄存器ebx表示基址,寄存器esi表示偏移量,mov eax,[ ebx + esi ];
注:寻址须加上取址符[ ]
list DWORD 0x10h,0x20h,0x30h,mov eax,[list+2];此时eax中存放的是0x20h,这种情况下
相对偏移= 类型*偏移
标志位
进位标志:无符号整数发生溢出;
溢出标志:有符号整数发生溢出;
零标志位:结果是否为零;
符号标志位:运算结果是否为负;
过程
使用call可以调用过程(函数);
堆栈
运行时栈是由CPU直接管理的内存数组,它使用两个寄存器:SS和ESP;ESP寄存器放的是指向堆栈内特定位置的一个32位偏移值,我们很少直接操纵ESP的值,通常是由CALL、RET、PUSH、POP等指令间接修改的;
堆栈指针寄存器(ESP)指向最后压入或添加到堆栈上的数据;
堆栈的作用:
1.寄存器在用作多种用途的时候,堆栈可以很方便的临时保存其值;在寄存器使用完毕后,可通过堆栈恢复其原始值;
2.CALL指令在调用的时候,堆栈可以用来保存其返回值;
3.调用过程时,可以通过压栈传入参数;过程中产生的局部变量在堆栈上建立;
过程执行
执行过程时,可以通过寄存器传递参数;
在过程中使用寄存器时,需要先将寄存器值压入栈中,使用完后再从栈中取出,确保之前的值不被覆盖;
CPU总是执行EIP寄存器所指向内存地址处的指令;
过程传递参数
1.寄存器传递参数:在调用函数前,将参数保存到寄存器中,在过程中获取寄存器的值;
2.堆栈传参,ESP寻址
PUSH 1 //压入参数1,调用完后手动弹出栈
PUSH 2 //压入参数2,调用完后手动弹出栈
CALL XXX //调用函数
XXX:
MOV EAX,DWORD PTR SS:[ESP+8] //包含函数的返回地址
ADD EAX,DWORD PTR SS:[ESP+12]
RETN 8 //将栈返回到压入参数前的位置
3.堆栈传参,EBP寻址
PUSH 1
PUSH 2
CALL XXX
XXX:
PUSH EBP
MOV EBP,ESP
SUB ESP,12 //提升栈
MOV EAX,DWORD PTR SS:[EBP+12] //通过EBP获得参数 参数位置 +4
ADD EAX,DWPRD PTR SS:[EBP+16]
MOV ESP,EBP
POP EBP //恢复栈
4.过程调用图
可以看到一个过程调用的结构大概是:本次过程调用参数 -> 返回地址 -> 上一个EBP地址 -> 本次调用栈空间分配 ;
条件处理
布尔指令
AND(与)、OR(或)、XOR(异或)、NOT(非)、TEST(与操作,不改变目的操作数,只是设置CPU标志位);
布尔指令会影响零标志ZF、进位标志CF(无符号超过限制)、溢出标志OF(有符号超过限制)、SF符号标志和奇偶标志;
布尔指令详解
- AND指令
在每对操作数之间进行与操作,并把结果存入目的操作数中; - OR指令
在每对操作数之间进行或操作,并把结果存入目的操作数中; - XOR指令
在每对操作数之间进行异或操作,并把结果存入目的操作数中; - NOT指令
对一个操作数的所有数据取反,得到的结果称为该操作数的反码;
NOT指令不影响任何标志位;
条件处理
- CMP指令
CMP在源操作数和目的操作数之间进行隐含的减法操作,两个操作数都不会被修改;
条件跳转指令
条件跳转指令在标志条件为真时分支转移到新的目的标号处,标志条件为假时继续执行后续指令;
格式 :Jcond 目标地址
(1)基于特定的标志值;
- jz/jnz
零标志或非零(零标志清楚)跳转; - jc/jnc
设置了进位或未设置进位跳转; - jo/jno
设置了溢出或未设置溢出跳转; - js/jns
设置了符号或未设置符号跳转; - jp/jnp
设置了奇偶标志或未设置奇偶标志跳转;
(2)根据两个操作数是否相等,或根据ECX的值;
- je/jne
(不)相等则跳转 - jcxz/jecxz
(e)cx等于零则跳转
(3)基于有符号数的比较结果;
(4)基于无符号数的比较结果;
(5)无条件跳转:jmp;
整数算术指令
移位指令(移出位送入CF)
- SHL(逻辑左移)
- SHR(逻辑右移)
- SAL(算术左移)
- SAR(算术右移)
- ROL(循环左移)
- ROR(循环右移)
- RCL(带进位循环左移)
- RCR(带进位循环右移)
- SHLD(双精度左移)
- SHRD(双精度右移)
算数指令优化(略)
内存管理
注:内存管理用于理解程序的运行过程,以下都是windows IA32处理器为基础
段概念:是一块供程序存放代码或者数据的长度不定的内存;
分段概念:把多个内存段互相隔离的方法,使得多个程序可以互相隔离运行而互不干扰;
段描述符概念:用来描述一个内存段的64位值,其中包含了段的基地址、访问权限、长度限制、段的类型和使用方式等信息;
段选择子概念:存放在段寄存器(CS、DS、SS、ES、FS、GS)中的值;
逻辑地址概念:一个段选择子和一个32位偏移地址的组合(因为用户程序从不直接修改段寄存器,一般只会用到32位数据偏移);
进程地址到物理地址的转换
(1)方式一:将进程中变量的段和偏移地址合成一个线性地址,这个线性地址即对应着物理地址;
(2)方式二:分页技术(使用虚拟地址空间);第一步将变量地址转换为线性地址,第二步使用页面转换方法把线性地址转换为物理地址;
在windows中,实地模式采用的是方式一,将整个进程装入内存中,进程的寻址范围是1MB;保护模式采用的是方式二,每个进程都有虚拟的4GB内存,通过操作系统将虚拟内存地址转换到实际的物理地址
线性地址
处理器如何使用段和偏移地址来确定一个变量的线性地址;每个段选择子指向描述符表里面的一个段描述符,段描述符包含了段的基址,通过逻辑地址的32位偏移与段的基址相加就得到了线性地址。线性地址是0到0xFFFFFFFF的32位整数(32位操作系统),他代表内存中的一个位置,如果分页机制没有打开的话,那么线性地址实际上就是数据的物理地址。
描述符表
局部描述符表中的索引如下图:
描述符表:段描述符表存在于两种类型的表中,全局描述符表(GDT)和局部描述符表(LDT);
全局描述符表:系统只存在一个全局描述符表,表的基址放在GDTR(全局描述符表寄存器)里面。表项(段描述符)指向各个段。操作系统可以把所有程序都要使用的段存放在GDT中。
局部描述符表:在一个多任务的操作系统中,每个程序都有自己的段描述符表,这个表称为局部描述符表。当前进程的LDT的基址存放在LDTR(局部描述符表寄存器)中。表项(段描述符)都包含了段在线性地址空间的基址;如上图所示,图中显示了三个不同的逻辑地址,每个地址对应的LDT中不同的表项。在这个图中,假设关闭了分页机制,所以计算出来的线性地址空间直接指向物理地址空间。
分页机制
分页机制是多任务运行的基础,它使得计算机同时在内存中装入多个进程(进程的内存总和大于物理内存)成为了可能。在一开始,处理器仅仅装入程序的一部分,剩余的部分保留在磁盘上,程序被装入的内存被划分为称为页的小块,通常一页的大小为4kb。物理内存也被划分为一个个的页框,用来装载程序运行的页数据;运行程序时,处理器有选择地将内存中一些不用的页面释放,然后装入其他马上要被用到页面;
操作系统使用一个页目录和一系列页表来追踪内存中所有程序的页面使用情况。当一个程序访问线性地址空间某个地址时,处理器会自动把线性地址转换为了物理地址,这个转换就称为页面转换;如果所需要的页面没有在内存中,处理器会打断程序的执行,并产生一个缺页中断,操作系统捕获这个错误并在程序恢复运行前把所需要的页面从磁盘上加载到内存中去。从应用程序的角度来看,缺页错误和页转换都是自动发生的。
页面地址转换
当分页机制被允许的时候,处理器必须把32为的线性地址转换为32位的物理地址,在这个过程中要用到以下三个数据结构:
(1)页目录:一个最多包含1024个32位表项的页表地址表。
(2)页表:一个最多包含1024个32位表项的页地址表。
(3)页:一个4kb或4mb的地址空间。
一个使用了分页机制的线性地址(个人认为这里的线性地址同样也是段描述符+偏移,但这里的段描述符的表示的不再是上述的段基址)可以分为三个部分:指向页目录的指针、指向页表的指针和在页中的偏移地址。当线性地址转换为物理地址的时候,处理器执行了以下步骤: