汇编是半开卷,可以带纸质资料。理论上,学好了以后,带本书进去就ok了,但是这次是线上,我还没书,就对着考试重点整理一点资料用于打印吧。
因为是线上,所以第4章基本不考框架了,浮点操作,伪指令之类的还考。第五章多一些,但也就那样,注意反汇编。
第一章:基础
性能指标计算
- 主频。CPU内部的频率,代表CPU的处理速度。
- 外频。CPU和外部(通常指内存)的通信频率,影响通信速度,如果通信速度太慢,会限制性能。
- 倍频。主频=外频×倍频
带宽=频率×位宽÷8
DDR技术下,一个周期可以有两次数据传送,所以带宽翻倍。
储存器原理
逆序储存:储存的基本单位是自己,字节内部是从高到低,但是字节之间按照低地址到高地址方式排列
最高位/最低位:在任何储存都适用。MSB,Most Significant Bit, LSB ,Least Significant Bit
字节,字,双字,四字。
数字到储存:0ED66025H在计算机中是25 60 D6 0E。注意前缀,如果第一个是数字,前缀可加可不加,如果第一个是字母,加0前缀和变量名区分。
储存到数字:先确定区间,再逐字节倒着写到纸上。
练习题
第二章:微处理器管理模式
CPU工作模式
通过修改控制寄存器CR0的控制位PE(位0)来实现从实模式切换到保护模式。
虚拟8086
模式;特权0(最高,OS),1,2,3
TODO
实模式
这是比较古老的模式了。1 不管实际地址线有多宽,只使用低20位地址线,即寻址空间为1M,很小。2. 内存分区,用内存段首地址+偏移访问。3. 任何区域都可以被访问,分区但不设禁区 4. 不支持并行
保护模式
现在最常用的就是保护模式了。保护模式基本是把实模式的缺点都优化了。1 使用32位地址线,支持4G内存,PentiumCPU以后扩展到36位,64G内存 2. 内存页式储存,段式储存 3. 提供基于特权级的内存保护机制4. 支持多任务并行 5. 引入虚拟内存(即用磁盘虚拟出内存来,虽然速度慢,但是可以保证很多大内存程序的运行)
虚拟8086模式(V86模式)
类似于虚拟机。实际上运行在保护模式上,只是虚拟出一个实模式。你即使把实模式搞坏了,也只是搞坏了我规定出来的区域。
寄存器
概述
- 通用寄存器。RAX-RDX。RBP,RDI,RSI。R8-R15
- 部分专用寄存器。RIP(指令指针),RSP(堆栈寄存器)
- 标志寄存器。0就是正常情况,1是非正常情况。0是常态,只有发生了特定变化才会变成1。具体: 1. D0:进位标志CF(Carry Flag),0代表没有进位(正常情况),1代表执行结果进位 2. D2:奇偶标志PF(Parity Flag),Intel微处理器采用奇校验。0代表执行结果的低八位中有奇数个1(正常情况)。比如11011010B有5个1,则PF=0 3. D6:零标志ZF(Zero Flag),0代表结果非0(正常情况),1代表结果为0 4. D7:符号标志SF(Signal Flag),0代表非负(正常情况),1代表结果是负数。D6和D7配合起来可以精确判断结果是正负还是0。 5. D10:方向标志DF(Direction Flag),针对字符串操作指令中地址变化方向。0代表增址(正常情况),1代表减址 6. D11:溢出标志OF(Overflow Flag),带符号数运算的时候,不溢出为0,溢出为1 7. D21:微处理器标识标志ID(Identification),相当于一个CPU指针,使用CPUID指令可以获取CPU信息,常有软件利用CPUID作为机器码,可以用于保护版权,但是虚拟机出现后这种方法就被破解了。
- 段寄存器。存放数据段基址,涉及到段式管理 1. CS:Code 代码段寄存器 2. DS:Data 数据段寄存器 3. SS:Stack 堆栈段寄存器 4. ES:Extra 附加数据段寄存器 5. GS: 6. FS:
GDTR(Global Descriptor Table Registr)全局描述符表寄存器
实模式下,采用两个16位表示20位地址。段寄存器存放段基址的高16位地址,然后加上第二个16位(逻辑地址)就行。
保护模式用GDTR和段寄存器寻址。
段选择子:保护模式下,16位段寄存器进化为段选择子,13位index用于选择,1位T1(区分GDT和LDT),2位RPL。
段描述符:8字节,有32位段基址,对应4G寻址空间。还有段限长,段属性字段。
GDTR:32位基址,16位限长,指向的GDT中最多存放
2
13
2^{13}
213个段描述符,正好与段选择符的13位index对应。
寻址过程:1. 寻找GDTR。使用LGDT(Load GDT)指令将GDT的基地址装入GDTR,前32位就是基址,同时后16位还可以确定GDT有多大,可以存多少个段描述符。注意,是后(16位+1)/8个段描述符。之所以要+1,是因为限长为0总得有意义吧,所以就统一加一,这样限长为0代表长度实际是1,最大0xFFFF实际是0x10000,还能凑个二进制整。2. 寻找段描述符。使用段选择子,生成段描述符在GDTR上的偏移量(要×8),用这个偏移量+GDTR基地址就是段描述符基地址。3. 寻找内存段。读取8字节的段描述符,用32位找到内存基地址,搭配其他32位辅助信息使用这块内存。
例题:0 E003 F000 3FFH,对应基址0E003F000H,长度为3FFH+1,容纳数量为(400H/8)=80H,算的时候转二进制算。
LDTR
LDTR中不储存LDT地址,而是存了一个段描述符。
寻址流程:1. GDTR确定了GDT的位置和大小 2. 用LDTR作为段选择符,选择GDT中的一个LDT描述符 3. 用LDT描述符确定LDT的位置和大小。 4. 用段寄存器在LDT中选择内存的描述符 5. 用LDT中的段描述符寻找物理内存
IDTR
实模式用中断向量表,地址为0H。保护模式用IDT,可移动。
IDTR有48位,同GDTR。但是最多256中断,所以浪费了很多空间。不需要搭配段选择子(段选择子本身只是用来在GDT或者LDT中选的),系统有其他机制。
TR
TSS,任务状态段。TR是一个段选择子,也是在GDT上选TSS描述符。但是TR是16位全用了,所以不需要乘8,直接和GDT基地址相加即可。
内存管理
实模式:分段,段地址*10H+offset。每一个内存段的首地址必须是16的倍数,而且段的最大长度只能是64K(因为16位偏移量的限制)
保护模式:1. 16:32位地址 2. 段选后,基址+偏移量形成32位线性地址 3. 如果分页,则经过分页部件转换为32位物理地址,不用分页则线性地址=物理地址。
段选:CS:EIP整体寻址流程(假设T1=0,默认GDT)1. CS:EIP是虚拟地址 2. GDTR锁定GDT表的位置和大小 3. CS的Index从GDT中选一个段描述符,从里面解析出内存中对应的32位区域基址与大小 4. 解析后的基址+EIP偏移量就是线性地址,如果不进行分页,那就是物理地址。
分页:(一个页描述符是4字节,所以×的时候是4):1. 前10位为页目录索引。PDBR的全部+页目录索引×4,得到页表描述符。 2. 中间10位为页号。页表描述符前20位基址+页号×4,得到页描述符。3. 最后12位(4K页大小)为页内偏移。页描述符前20位基址+页内偏移,得到物理地址。
段描述符结构:3. 段限长+1才是真正的限长。4. S(System),S=0代表系统段描述符,用户不可用,1代表代码段、数据段、堆栈段,用户可用 5. E(Executable),在S=1的前提下,E=1代表代码段,可执行,E=0代表数据段、堆栈段,不可执行 6. DPL。特权级,格式同RPL 7. G(Granularity),粒度,为限长的单位,G=0是默认的Byte字节为单位,G=1以页为单位,一页占用
2
12
2^{12}
212,4KB的空间
任务
TSS是任务状态段,GDT有TSS对应的8字节描述符。
门用于转换,比如不同任务之间的调用,比如用户程序调用系统程序,转换过程中会考虑特权级。调用门对应调用,任务门用于任务切换。
保护
数据访问的保护:DPL ≥ MAX(CPL, RPL); CPL是当前正在运行的程序的特权级(CS);DPL是段描述符特权级;RPL是段选择子里的请求特权级。
对程序的保护:段间调用或跳转,需要检查限长,特权级CPL和DPL。CPL=DPL,允许跳转和调用。CPL<DPL,禁止。CPL>DPL,此时要检查目标代码段描述符的C位。如果C位为1,表示这是一致代码段,允许跳转和调用。(很多系统调用的C就是1)
对输入输出的保护:略
习题
指令系统
寻址方式
默认DS:偏移。下图,比例变址是基址:偏移,其他都是仅写偏移的写法
指令概览
注意:指令 dst,src。别搞反了。
数据传送指令:MOV(1. 常数肯定不能被赋值 2. 不可以随意用常数指定段寄存器,至少应该先送到寄存器中(段寄存器不可以给段寄存器赋值)3. CS是代码段,不可写 4. 两个内存不能传)、PUSH(至少16位)、POP、XCHG、IN(寄存器必须是累加器,如果端口号超过1字节要用DX存)、OUT、LEA、PUSHF(TODO浮点数)、POPF
二进制运算指令:ADD(CF=1的时候,代表无符号数溢出,OF=1的时候,代表有符号数溢出)、ADC(带进位(DST)+(SRC)+ CF → DST)、INC、SUB、SBB((DST)-(SRC)-CF → DST)、DEC、CMP(dst-src,不影响dst)、MUL(MUL SRCreg/m,字节型乘法:(AL)×(SRC)8→AX ,字型乘法: (AX)×(SRC)16→DX:AX,双字型乘法:(EAX)×(SRC)32→EDX:EAX)、IMUL、DIV(具体操作:余数在高部分,商在低部分 1. (AX)÷(SRC)8 2. (DX:AX)÷(SRC)16 3. (EDX:EAX)÷(SRC)32)、IDIV
逻辑运算指令:AND(按位与,影响dst)、OR、NOT、XOR、TEST(不影响dst)
移位指令:SHL(SHL dst,CNT。CNT是1就直接写,不是1就存CL后再移动(286以后的可以直接写立即数))、SAL、SHR、SAR、ROL(循环)、ROR、RCL(带进位循环)、RCR(普通循环是直接把移出位弄到补充位上了,带进位的是先把这一次移出的位放到CF,然后把上一次的CF放到补充位上。是用CF做一个缓冲)
程序控制指令:JMP(直接的短转移和近转移都是相对寻址,其他的都是赋值。注意赋值顺序是从低到高。比如JMP 12 34 56 78实际上对应78 56: 34 12。),JXX(下图)、循环指令(LOOP:短转、CX,先减后判断,0比较特殊)、子程序指令:CALL、RET、RET n、中断指令:INT n、IRET(中断处理程序中的返回)
注意有符号和无符号。对于有符号数跳转,用G和L,对应Greater和Less。
对于无符号数跳转,用A和B,对应Above和Below。
标志操作指令:STX/CLX命令。X可以是D(对应DF),C(CF)。CL是清0,ST是置1。NOP(空指令)
串操作指令:思路(先确定两个段基址,然后确定两个段起始偏移。之后确定重复次数,重复搬运。),DF(修改SI,DI指针,方向由DF决定,0就是正常,1就是反向)、指针(每次移动举例由MOVSX的X决定,如果是MOVSB使SI、DI各减1)、REP、MOVSB/W/D(内存间搬运)、STOSB/W/D(STOS配LODS,先弄到累加器,处理后再搬运到目的地)、LODSB/W/D、CMPSB/W/D、SCASB/W/D
例:把自AREA1开始的100个字数据传送到AREA2开始的区域中。
MOV AX,SEG AREA1)//先把段基址以AX为介质,加载到DS,ES中
MOV DS,AX
MOV AX,SEG(AREA2)
MOV ES,AX
LEA SI,AREA1 //将段偏移初始化
LEA DI,AREA2
MOV CX,100 //循环次数
CLD //保证DF=0
REP MOVSW //重复移动,每次移动一个word
;100个字数据传送完毕后执行下一条指令
例:把自NUM1开始的未压缩BCD码字符串转换成ASCII码,并放到NUM2中,字符串长度为8字节。设DS、ES已按要求设置。
LEA SI,NUM1
LEA DI,NUM2
MOV CX,8 //循环8次
CLD //确保DF=0
LOP:
LODSB //取Byte到AL
OR AL,30H
STOSB //把AL送到DST
LOOP LOP
习题
汇编程序开发
这一章线上不好考,能看懂代码就行。
注意浮点运算。访问的时候用st(i)就相当于使用索引为i的寄存器。需要注意的是,这是一个栈,而不是数组,比如你st(0)储存了1.2,此时你再push进来一个数2.3,则1.2就会被挤到st(1)的位置,而st(0)永远代表栈顶。
数据传送(1. FLD与FST。入栈用FLD(load),出栈用FST(store)。2. FSTP。FST不等同于pop,只是将数据store到内存,并没有pop操作,所以FSTP相当于FST+POP。3. FLDPI。数据传送有一种特殊的情况,就是我们要将一些特殊常数传入栈中,比如π,我们肯定不能手写,必须用特殊指令:FLDPI加载到栈中。还有一些类似的无理数,略过。)
算数运算指令。(指令看起来很复杂,其实就是加减乘除4大类二元运算,就是在整数指令前加个F,每一类有5种写法:1. FADD dst,src。这是最常用的,相加,送到dst中。2. FADD src。这是次常用的,默认将结果送到s(0)中 3. FADD和FADDP。这两个都会执行pop,将新的结果送到栈顶,也就是原来的st(1))
超越函数指令(FSIN,可以将st(0)变成sin(st(0)),sin,cos,tan,atan写法都一样)
子程序设计
完整定义:子程序名 PROC [C | stdcall] :[第一个参数类型] [,:后续参数类型]
PUBLIC 名字[,…]
EXTRN 变量名:类型[,…]
子程序名 PROTO [C | stdcall] :[第一个参数类型] [,:后续参数类型]
PUBLIC和EXTERN用法:(1. public和extrn必须在data区之前就写好。2. public可以对变量和函数使用,都不需要带类型。注意,变量必须是data区里的变量(有点像c语言中的全局变量),绝对不能是子程序里的局部变量 3. extern只能对变量使用,不能用于函数(是不能还是像C语言一样默认extrn?TODO)4. 如果想调用其他文件的函数,就是用PROTO方法声明。注意,声明用PROTO(函数原型),定义用PROC。)
栈帧构造,是先构造参数,之后压入返回地址,然后压入ebp与其他被调用者保护,最后开局部变量空间。下图代码基本展示了这一过程。图中从29开始到36,这几句是栈上的局部变量区初始化过程。其他的就都是我们上面描述的。
最开始push ebp(被调用者保护)。需要特别注意的是那句mov ebp,esp,ebp是esp刚将ebp压入后的值,esp后面开了内存以后还会移动。我们在程序中,取参数,取局部变量都是要用ebp的,而不是esp,esp随着栈帧结构变化而变化。最后子程序结束,要恢复esp的时候会mov esp,ebp,将esp恢复到刚压入ebp的时候,之后再pop掉栈里的ebp(被调用者保护恢复)与返回。
如何用ebp取参数和局部变量呢?首先要明确栈的方向,地址减小方向是栈顶方向,也是局部变量方向。
- ebp+n是取参数,n越大,代表参数越靠后。注意,ebp取参数要跳过被调用保护寄存器和IP指针。在前面的很多程序中,IP指针一般是压EIP,占4字节,还有一个保护EBP,总计8字节,所以取的第一个参数通常是ebp+8。
- ebp-n是取局部变量。奇妙的是,局部变量也是n越大,代表声明顺序越靠后。
举个例子分析一下顺序问题:假设有两个局部变量,两个参数,都是DWORD,那么第一个参数就是ebp+8到ebp+11,第二个参数是ebp+12到ebp+15。第一个局部变量是ebp-4到ebp-1,第二个局部变量是。ebp-8到ebp-5。总之就是,越靠近ebp的,就越是先定义的,绝。
反汇编后的三类变量名字的特点:(1. 全局。C语言直接声明,汇编中用_i1这种带下划线的标号,且要public声明 2. 静态全局。C语言中加static,汇编中用i2这种正常标号,放在程序data段 3. 局部变量。C语言中在函数中声明,汇编中使用ebp进行偏移寻址。)