介绍
我们都知道在计算机里只有高低电平方式来区别,就如摩尔电码一样,特定的组合代表特定的意义,这就是以二进制的形式传递或者存储数据。而以二进制的形式代表的指令集一般叫机器语言,可是机器语言对于人类来说是非常麻烦的,所以为了简化编写机器语言,汇编就诞生了。对于不同的平台对应的机器语言不同,汇编和机器语言是一一对应的,所以汇编是不能移植到其他平台。
cpu架构
最早出现的cpu是intel的一款cpu,但是以前的intel的cpu那时还没有申请专利,还是小公司,所以cpu的结构是和其他公司共享了,但是后面公司大了,申请专利了以后,其他公司也都已经有了技术去生产改进8086的cpu,以至于后面cpu都是利用8086来改进生产的,例如知名的amd公司的cpu。而8086之后出现的cpu都会兼容8086的指令集,所以我们将这些兼容的8086的指令集叫做X86架构。
CPU的结构
CPU也就是中央处理单元,包含了有限的寄存器,一个高频时钟,一个控制单元,一个算数逻辑单元。作用如下:
- 时钟 (clock) 对 CPU 内部操作与系统其他组件进行同步。
- 控制单元 (control unit, CU) 协调参与机器指令执行的步骤序列。
- 算术逻辑单元 (arithmetic logic unit, ALU) 执行算术运算,如加法和减法,以及逻辑运算,如 AND(与)、OR(或)和 NOT(非)。
CPU 通过主板上 CPU 插座的引脚与计算机其他部分相连。大部分引脚连接的是数据总线、控制总线和地址总线。
附加:总线 (bus) 是一组并行线,用于将数据从计算机一个部分传送到另一个部分。一个计算机系统通常包含四类总线:数据类、I/O 类、控制类和地址类。数据总线 (data bus) 在 CPU 和内存之间传输指令和数据。I/O 总线在 CPU 和系统输入 / 输出设备之间传输数据。控制总线 (control bus) 用二进制信号对所有连接在系统总线上设备的行为进行同步。当前执行指令在 CPU 和内存之间传输数据时,地址总线 (address bus) 用于保持指令和数据的地址。
处理器的寻址地址
我们以16位的8086来作为例子,内部结构有16位双向数据信号线,20位地址信号线,可寻址1m字节的存储单元,对应地址为0x00000-0xFFFFF 。
由于存储地址是20位,但是寄存器是16位的,所以他们采用了分段存储的方式 来解决这个问题,一个段位64kb大小,这些就是所谓的段寄存器。但是段的地址可以允许部分重叠,完全重叠、连续排列等。
而暂存单元里面有些地址是固定使用的,用户不能随便访问和改变,中断矢量区:00000H—003FFH。显示缓冲区:B0000H—B0F9FH。彩色显示器的显示缓冲区:B8000H—BBF3FH。启动区:FFFF0H—FFFFFH。
寄存器
8086内部的寄存器可以分为通用寄存器和专用寄存器两大类,专用寄存器包括指针寄存器、变址寄存器等。
8086有4个16位的通用寄存器(AX、BX、CX、DX),可以存放16位的操作数。其中AX称为累加器,BX称为基址寄存器,CX称为计数寄存器,DX称为数据寄存器。
有两个16位的指针寄存器SP和BP,其中SP是堆栈指针寄存器,由它和堆栈段寄存器SS一起来确定堆栈在内存中的位臵;BP是基数指针寄存器,通常用于存放基地址。
有两个16位的变址寄存器SI和DI,其中SI是源变址寄存器,DI是目的变址寄存器,都用于指令的变址寻址方式。
有两个16位的控制寄存器IP和标志寄存器,其中IP是指令指针寄存器,用来控制CPU的指令执行顺序,它和代码段寄存器CS一起可以确定当前所要取的指令的内存地址。标志寄存器的内容被称为处理器状态字PSW,用来存放8086CPU在工作过程中的状态。
标志寄存器的各个位代表的意义:
标志名称 | 溢岀 | 方向 | 中断 | 符号 | 零 | 辅助进位 | 奇偶 | 进位 |
---|---|---|---|---|---|---|---|---|
符号 | OV | UP | EI | PL | ZR | AC | PE | CY |
有4个16位段寄存器,即代码段寄存器CS、数据段寄存器DS、堆栈段寄存器SS和附加段寄存器ES。这些段寄存器的内容与有效的地址偏移量一起,可确定内存的物理地址。通常CS划定并控制程序区,DS和ES控制数据区,SS控制堆栈区。
计算机重启后会对cs赋值为FFFF0,其他寄存器全部为0。
汇编指令
结构
指令是一条语句,编译器会将语句逐条转换成机器语言指令,让cpu去加载和执行。
一条指令由四个部分组成:标号,指令助记符,操作数,注释。例如:
1 | [label: ] mnemonic [operands] [;comment] |
保留字
这是汇编有特殊意义的字符且不分大小写。有以下保留字:
$ ? @b @f addr basic byte c carry? parity? pascal qword real4 real8 real10 sbyte sdord sign? dword far far16 fortran fword near near16 overflow? stdcall sword syscall tbyte vararg word zero?
mov
mov指令是进行赋值操作的,格式为:mov 目的操作数,源操作数 。目的操作数必须是一个容器。且两个操作数的数据宽度是一致的,不能同时都为内存单元。而且不允许立即数直接赋值到段寄存器中。
例如:mov [0xb800],0x0100。这句的意思是把0100这个数据存储到ds寄存器的0xb800地址里。如果没有指定寄存器,那么默认就是ds。mov ax,0x0100是将0100赋值到ax里。mov ax,dx 。mov ax,[0x0100] ……
但是在后面出现了新的技术,叫符号扩展,我们的源操作数的宽度可以小于目的操作数了,对于有符号和无符号的数,扩展是不一样的,所以提供了两个操作指令。movzx是无符号的数,movsx是有符号的数。movsx将对扩展位赋值为1,而movzx将赋值为0。
db,dw,dd,dq
db指令是让编译器帮我们声明以及初始化一些数据,每个数据的宽度为1字节。dw每个数据的宽度为2个字节,dd为4个字节,dq为8个字节。这些指令称为伪指令。
伪指令一个重要的功能就是定义程序区域段。
=
=是伪指令,作用是让后面出现的标记替换成标记所存储的变量值。
1 | count byte 2233; |
如果后面出现许多个count标记,那么那些标记都会替换为2233,但只限于这条指令后面的标记,前面的不换。extra:dup()可以存储数组和字符串。
proc和endp
这两个是伪指令,是创建过程的指令,过程也就相当于c语言的函数一样,是一个代码区间块,而且这两个指令是成对出现的。格式是:
1 | simple proc |
ret是让cpu强制返回到调用过程的地址。而过程的标记只会在其过程中可见,在外部是不可见的。所以如果需要打破这个限制,就使用标记::的形式来定义全局标记,进行跳转和调用。
call和ret
call是调用过程的指令,而ret是过程返回的指令。call实际上是将下一条指令的地址压入栈里,然后将ip寄存器保存为过程的第一条指令的地址。而ret实际上是将栈的第一个地址弹出栈,然后将这个地址保存在ip寄存器里。
div
div指令是做除法的使用,而且分为多种情况。
第一种16位除8位。格式为:div 除数 。除数是8位通用寄存器或者是内存单元提供。而被除数是保存在ax中的,是固定的。除完以后会覆盖ax的被除数,ah保存余数,al保存商。
第二种32位除16位。格式为:div 除数 。除数是16位通用寄存器或者是内存单元提供。dx保存被除数高16位,ax保存被除数低16位。除完后商保存ax,余数保存dx。
xor
xor指令是异或指令,格式:xor 目的操作数,源操作数。作用是对比两个操作数的每一位,相同为0,不同为1,然后将值重新保存在目的操作数里面。
add和sub
该指令的限制和mov一样,add是将两个操作数相加然后存放在目的操作数的容器里。sub则是将目的操作数减去源操作数,然后存放在目的操作数里。
lahf和sahf
lahf是将标志寄存器里面的低字节复制到ah里。而sahf则相反,将ah里面的数据复制到标志寄存器里。
xchg
xchg是将两个操作数的数据交换,不能使用立即数作为操作数。
inc和dec
inc是将操作数加1,而dec是将操作数减1。例如
1 | inc 1000h; |
neg
NEG(非)指令通过把操作数转换为其二进制补码,将操作数的符号取反。
uses
uses是调用过程将寄存器压入,返回将寄存器弹出的自动生成代码的指令,这样我们就不用写这些代码了。但是要返回的寄存器是不能使用这个的,不然会数据丢失。例如:
1 | simple proc uses eax ecx ebx |
jmp和loop
这两个指令都是跳转到指定地址的指令,但是jmp是无条件跳转,而loop是有条件跳转。loop的跳转叫做ecx计数器循环,每次循环后都会将ecx的数据减1。如果ecx为0的话就不会循环。但是如果ecx初始化为0后,会执行宽度最大值的循环,因为每次循环最先执行ecx-1的操作,然后用值比较0。还有在实地址模式下,默认的是cx寄存器。loop跳转的范围是有限的,必须距离当前地址计数器-128到127字节范围类。例如:
1 | flag: |
运算符
offset
offset是返回数据标号的偏移量,这个偏移量按字节计算,表示的是该数据标号距离数据段起始地址的距离。
ptr
ptr可以让数据标号的数据宽度重新改变。程序可能需要将两个较小的值送入一个较大的目的操作数。如下例所示,第一个字复制到 EAX 的低半部分,第二个字复制到高半部分。而 DWORD PTR 运算符能实现这种操作:
1 | wordList WORD 5678h,1234h |
type
type是返回变量的单个元素的大小,这个单个元素以字节为单位。如果变量大小是字节,返回为1,如果是字,返回为2。
lengthof
LENGTHOF 运算符计算数组中元素的个数,元素个数是由数组标号同一行出现的数值来定义的。如果数组定义中出现了嵌套的 DUP 运算符,那么 LENGTHOF 返回的是两个数值的乘积。例如:
1 | myArray BYTE 10,20,30,40,50, |
label
LABEL 伪指令可以插入一个标号,并定义它的大小属性,但是不为这个标号分配存储空间。常见的用法是,为数据段中定义的下一个变量提供不同的名称和大小属性。例如:
1 | .data |
typedef
TYPEDEF 运算符可以创建用户定义类型,这些类型包含了定义变量时内置类型的所有状态。我们一般使用它去建立指针,因为建立的变量保存了目的变量的地址。
1 | PBYTE TYPEDEF PTR BYTE ;字节指针 |
逻辑移位
逻辑移位就是将目的操作数向左或者是向右移动位置,无论是向什么方向,空出来的位置都由0填补,而移出去的位保存在进位标志里(cf)。
shl是逻辑向左,shr是逻辑向右移位。例子如下:
1 | shl ax,2 ;ax内存数据向左移动2位 |
算术移位
算术移位和逻辑移位差不多,但是就是算术移位在向右移位时,会在高位以符号位的形式补上。所以是有符号数的使用。
sal是算术向左,asr是算术向右。使用方法和逻辑移位一样。
循环移位
循环移位也是移动位的位置,但是移出去的数会在另一端出现,也会保存在进位标志里。
rol是循环向左,ror是循环向右。例子如下:
1 | ; ah 1000 1100 |
条件判断
and
and指令是按位与的指令,就是目的操作数的每一位和源操作数进行与操作,结果保存在目的操作数中,and指令一般的作用都是将数据位进行屏蔽的,也就是复位和清除的功能。例如:
1 | ;ax 10110111 11101010 |
or
or指令是按位或的指令,就是目的操作数的每一位和源操作数进行或操作,结果保存在目的操作数中,or操作是将需要的位置为1,其他位不变,作用和and恰恰相反。这两个指令通常在标志寄存器里面用的非常频繁。
1 | ;ax 10110111 11101010 |
cmp
cmp指令是用于比较目的操作数和源操作数的指令,比较的过程是让目的操作数减去源操作数,会影响进位标志位和零标志位,溢出和符号标志位,不会保存结果。无符号数的比较和有符号数的比较,影响的是不同的标志位。一般配合jmp指令作为if条件的判断跳转。
not
not指令是将操作数按位取反的功能。
test
test指令是让两个操作数之间做and操作,但是不会改变操作数。
跳转指令
JC | 进位跳转(进位标志位置 1) |
---|---|
JNC | 无进位跳转(进位标志位清零) |
JZ | 为零跳转(零标志位置 1) |
JNZ | 非零跳转(零标志位清零) |
JO | 溢出跳转 |
JNO | 无溢出跳转 |
JS | 有符号跳转 |
JNS | 无符号跳转 |
JP | 偶校验跳转 |
JNP | 奇校验跳转 |
JE | 相等跳转 (leftOp=rightOp) |
JNE | 不相等跳转 (leftOp M rightOp) |
JA | 大于跳转(若 leftOp > rightOp) |
JNBE | 不小于或等于跳转(与 JA 相同) |
JAE | 大于或等于跳转(若 leftOp ≥ rightOp) |
JNB | 不小于跳转(与 JAE 相同) |
JB | 小于跳转(若 leftOp < rightOp) |
JNAE | 不大于或等于跳转(与 JB 相同) |
JBE | 小于或等于跳转(若 leftOp ≤ rightOp) |
JNA | 不大于跳转(与 JBE 相同) |
JG | 大于跳转(若 leftOp > rightOp) 有符号数 |
JNLE | 不小于或等于跳转(与 JG 相同) 有符号数 |
JGE | 大于或等于跳转(若 leftOp ≥ rightOp) 有符号数 |
JNL | 不小于跳转(与 JGE 相同) 有符号数 |
JL | 小于跳转(若 leftOp < rightOp)有符号数 |
JNGE | 不大于或等于跳转(与 JL 相同)有符号数 |
JLE | 小于或等于跳转(若 leftOp ≤ rightOp)有符号数 |
JNG | 不大于跳转(与 JLE 相同)有符号数 |