汇编基础
一、汇编语言的位置
为什么要学习汇编语言呢?就像学习美国文化要先懂英语一样,如果不了解计算机的语言,又何谈懂计算机呢?我们知道,计算机执行的语言,或者称之为命令序列或数据,都是以“1和0”的二进制语言,物理上则表现为电信号的高低电平。虽然我们现在有C/C++、Java、Python等一系列强大的高级语言,但是其真正落实到计算机的执行时还是需要编译或解释成计算机懂的机器语言。最早的时候计算机编程就是在纸带上打孔表示1或0,以此作为编程指令,不仅难于操作,而且很难查错,记忆也很困难。于是人们发明了机器语言的“人性化”表示:汇编语言(Assembly language),每条汇编指令都对应着一条机器二进制串,但是却更加容易理解和记忆:
虽然我们现在有许多的高级语言可以用来编程,但是如果想真的理解代码执行的实际过程,还是需要我们去懂些汇编语言,从本质上理解机器的行为。因此当今IT的各位童鞋们学习汇编语言还是十分重要的,逆向就更不用说了。
二、机器架构
了解了汇编语言的位置,接下来需要看看机器的架构。计算机的核心是CPU,负责各种运算,其中又包括运算器和寄存器,寄存器中也用来存储数据,但是一般较小,读写速度快,是运算器直接操作的对象;CPU之外有存储器,存储器一般指内存,分为可读写的RAM和只可读的ROM;其余各种外设比如显卡、网卡等都通过主板上的总线与CPU相连,CPU通过总线同存储器以及各种外设中的芯片(进而同外设中的存储器)进行数据通信。
存储器按字节大小分为存储单元,例如一个字节是一个存储单元。CPU访问存储器时必须完成三件任务:1. 存储单元的地址(地址信息);2. 器件的选择,读或写的命令(控制信息);3. 读或写的数据(数据信息);因此,CPU同各种存储器之间自然而然存在三种总线:地址总线、控制总线与数据总线。
控制总线决定了命令的种类,数据总线决定了一次处理的数据最大位数,8位数据总线处理16位数据时要分两次进行,而16位数据总线一次就能解决问题。地址总线则决定了CPU的寻址能力,即内存的大小,比如8位的地址总线最大只有256个单元,即256字节,而32位的地址总线最多有2的32次幂bit,即4GB,这也是x86最多支持4G内存的原因。
既然说到了4G内存的由来,那么就可以趁热打铁说说内存地址空间了,CPU认识的每个存储单元默认为1字节,因此可以定位的所有内存单元最大就是2的N次幂,N为地址总线长度,因此这个内存地址也就构成了计算机的内存地址空间,即所有可读写的存储器单元必须在这其中,否则就无法为CPU所定位检索。一般的架构是这样的:
这里0x9ffff是655539,0xbffff是786431,0xfffff是1048575,这样认识的会更清楚些(感谢Python的计算机功能,果然方便)。主存储器地址空间即内存,如果直接操作这个地址空间内的数据,效果就是直接读写内存数据;显存地址空间指的的是显存中的RAM部分,显存的ROM以及网卡等其他外设的ROM都在最后的空间中。
CPU中的主要结构是运算器、控制器与寄存器,这些器件通过CPU的内部总线相连,其中运算器负责信息处理,寄存器负责信息存储,控制器控制各种器件进行工作,内部总线连接各种器件,在它们之间进行数据的传送。对于汇编程序员来说,主要部件是寄存器,因为只有寄存器是我们可以编程直接操作的。不同的CPU架构不同,8086CPU共有14个寄存器,分别是AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW,今天我们先来学习基础的通用寄存器,其余的寄存器在用到时会给予说明。
一、通用寄存器
AX、BX、CX和DX是四个通用寄存器,通常用来存放一般性的数据,后面的分析都在8086CPU中进行。每个通用寄存器是16位,即一次可以处理一个字(2个字节)的数据,但是为了与8086CPU之前的CPU相容,也支持一个字节的寄存器,即AH与AL,类似的还有BH和BL等,CPU操作时会降AH和AL当作独立的寄存器,运算进位时会直接丢弃,因为CPU认为只有一个8位的寄存器而已:
二、物理地址
CPU访问内存需要知晓存储单元的地址,由于8086CPU地址总线为20位,但是寄存器却只有16位,即一次处理的地址最多只有2的16次幂,远小于2的20次幂。为了弥补这个问题,8086CPU采用了一种特殊的方式通过16位的寄存器来构造20位的访问地址,简单来说,即:段地址*16+偏移地址。从计算的位数上来说,段地址和偏移地址存储在CPU寄存器中,都是16位的;段地址*16,即整体在右边添加4个二进制位,成为了20位;20位与16位相加,得到了20位的实际内存地址。这里虽然是8086CPU中的方法,但是其实确实现在所有CPU中的实际寻址算法,即一个基地址加上一个偏移量得到一个实际地址。由此我们可以得到段的概念,内存本身不分段,但是由于CPU的特殊寻址方式,我们可以将内存看作最大64KB的一个个段(16位的最大值正好是64KB),因此我们可以在汇编中人为的指定段的起始和结束。当然,这里的段地址和偏移量实际上存储在CPU的寄存器中,比如获取指令的CS:IP,其中CS为代码段寄存器,其中存储代码段的基地址,IP寄存器则存储着当前要执行的指令的指针,即一个偏移量,因此CS:IP指定了接下来CPU要执行的指令的位置。这里需要说明的是,内存中的数据对于CPU来说都是二进制位,能够区分数据和指令的唯一标准就是指令曾经或者正在被CS:IP指定;每执行完一条指令,IP会累加上条指令的长度,从而指向下调指令。
我们是否可以修改控制CS:IP的值呢?答案是肯定的,只不过我们不能使用mov等传送指令,而应当使用jmp这类转移指令,基本的用法是:
-1. 修改CS:IP: jmp 2AE3:3 执行后:CS=2AE3H, IP=0003H;
-2. 仅修改IP:jmp ax(ie. move ip, ax)即用寄存器中的值修改IP;
三、内存访问
CPU访问内存除了获取指令,还要获取数据,那么数据部分如何定位的呢?同指令的CS:IP一样,8086CPU使用DS:[...]来获取内存数据地址,其中段寄存器存储内存数据段的基地址,而[...]表示一个内存偏移量指向的内存单元,如[0]表示偏移量为0的内存单元,这里使用时要注意,8086CPU不支持直接对DS传送值,因此mov
ds, 1000H是非法的,正确地是通过寄存器来实现,即:mov ax, 1000H; mov ds, ax;
这部分我们要学习基本的汇编指令,如mov、sub、add等,都可以操作寄存器,操作完之后将数据放入第一个参数表示的寄存器中。CPU中的内存数据一种特殊的结果就是栈,即只能从一端读写数据的结构,其基本指令是push ax;将寄存器ax的值入栈;和pop ax;从栈中取出栈顶元素放入寄存器ax中;下面是mov\add\sub命令的一个简单示例:
然后我们来看看PUSH命令的执行过程:
然后是POP命令:
可以看出,栈的操作关键是栈顶位置的确定,因此CPU专门使用SS:SP来获取当前内存中栈顶的位置。值得一提的是,CPU本身并没有对栈的大小进行检查,因此实际中会出现栈顶越界的问题(上限超出-PUSH;下限超出-POP),这也就要求我们必须人为进行检查,否则就会出现程序的漏洞。
了解了汇编语言的寄存器的基础知识,这次来把现在自己想到的、剩下的知识点一起总结梳理下吧!当然这么短的时间不可能学的会汇编编程了,不过自己的初衷原本就不是掌握汇编,而只是读懂汇编;读懂汇编才能逆向分析,仅此而已,如果说以后可以在C/C++中嵌入汇编代码提高程序效率之类的问题当是以后的事情了。好了,闲话少叙,今天就来把剩下的汇编知识点“一网打尽”吧!
一、[BX]和loop指令
直接来解释下这两个指令吧!我们知道[0]可以表示偏移量为0的内存单元(段地址存在CS里),[BX}则提供了更为灵活的方式,即mov ax, [bx]的功能是寄存器bx中存放的数据作为一个便宜地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中,即(ax)=((ds)*16+(bx)),()表示寄存器中的值。实际上,[]就表示一个内存单元,其中的数值或寄存器则表示了偏移量的来源。
loop指令是循环指令,格式是loop 标号。当CPU执行loop命令时需要执行两步操作:1)(cx) = (cx)-1; 2) 判断cx中的值,不为零时则转至标号处执行循环体语句。比如程序:
点击(此处)折叠或打开
-
assume cs:code
-
code segment
-
mov ax, 2
-
mov cx, 11
-
s: add ax, ax
-
loop s
-
mov ax, 4c00h
-
int 21h
-
code ends
- end
二、and与or指令
and指令即逻辑按位与操作,利用此操作可以将操作对象的相应位设为0,其他位不变:
点击(此处)折叠或打开
-
mov al, 01100011B
-
and al, 00111011B
-
- al = 00100011B //执行后
点击(此处)折叠或打开
-
mov al, 01100011B
-
or al, 00111011B
-
- al = 01111011B
点击(此处)折叠或打开
-
mov bx, 0
-
mov ax, [bx]
-
-
mov si, 0
-
mov ax, [si]
-
-
mov di, 0
- mov ax, [di]
点击(此处)折叠或打开
-
mov bx, 0
-
mov ax, [bx+123]
-
-
mov si, 0
-
mov ax, [si+123]
-
-
mov di, 0
- mov ax, [di+123]
三、转移指令
可以修改IP,或同时修改CS和IP的指令统称为转移指令。8086CPU的转移行为有两类:
-1. 只修改IP,称为段内转移,如:jmp ax;
-2. 同时修改CS和IP,称为段外转移,如:jmp 1000:0;
至于call和ret指令,其实也是转移指令,从执行效果上看:
-1. CPU执行ret指令时,相当于进行:pop IP;
-2. CPU执行retf指令时,相当于进行:pop IP; pop CS;
当CPU执行call指令时,进行两步操作:
-1* 将当前的IP或CS和IP一起压入栈中;
-2* 转移
PS:好了,汇编的基础暂时告一段落,但是资料还是放在案头,学习的过程中有不懂的随时来查阅补充了。