掌握汇编语言的目的是能够深入理解计算机系统的底层原理,在提高程序设计能力的同时也为二进制安全打下坚实的基础。
目录
对于二进制领域来说,无论是逆向工程、漏洞挖掘、加解密等等都需要以精通汇编语言为首要条件,才可以做到对二进制相关技术的深入理解、深入学习,从而更好的掌握二进制安全相关的技能。
1.CPU指令集
1.1 CPU的作用
计算机系统中的CPU只能执行由机器指令组成的程序。一般我们使用的高级语言都是通过相应的编译器,将高级语言编写的代码编译成由机器指令组成的可以被CPU执行的程序,即目标代码。目标代码是全部由二进制编码形成的程序,即机器指令,可以直接被CPU执行。因此,CPU是计算机系统的核心,计算机上的任何操作最终都是由CPU执行机器指令进行相应的处理完成的。
CPU的基本功能是执行机器指令、暂存少量数据和访问存储器。CPU能够自动执行存放在存储器中的由若干条机器指令组成的目标代码,并且在CPU的寄存器中可以暂存少量数据,以提高处理效率。当待处理的目标代码或数据在存储器中时,CPU想要执行目标代码就需要访问存储器(这里指计算机的物理内存)。
1.2 CPU指令集
CPU能够直接识别并遵照执行的指令被称为机器指令,一款CPU能够执行的全部机器指令的集合称为该CPU的指令集。CPU决定指令集,所以不同种类的CPU,对应的指令集往往也不相同。
从现阶段的主流体系结构讲,指令集可分为复杂指令集(CISC)和精简指令集(RISC)两部分(指令集共有四个种类)。通常会把CPU的扩展指令集称为”CPU的指令集”。
RISC是精简指令集(Reduced Instruction Set Computing),常见的应用有ARM和IOS等。精简指令集只提供很有限的操作,基本上单周期执行每条指令,其指令长度一般固定为4个字节。CPU并不会对内存中的数据进行操作,所有的操作都要求在寄存器中完成,而寄存器和内存的通信则由单独的指令来完成。与CISC相比,RISC具有更多的通用寄存器可以使用,且每个寄存器都可以进行数据存储或寻址,能够非常有效的适用于采用流水线、超流水线和超标量技术,从而实现指令级并行操作,提高处理器性能。
CISC是复杂指令集(Complex Instruction Set Computing),x86CPU主要有intel的服务器CPU和AMD的服务器CPU两类。复杂指令集指令复杂丰富,功耗大,长度从1到6字节不固定,而且CPU可以直接对内存进行操作。
2.数据表示
2.1 数值数据表示
计算机系统中存储信息的最小单位是bit,只能表示两种状态,即二进制的0和1。除此之外,常用的进制有八进制、十进制、十六进制等,下表给出了常见进制可能的取值范围:
进制 | 基数 | 可取数值 |
二进制 | 2 | 01 |
八进制 | 8 | 01234567 |
十进制 | 10 | 0123456789 |
十六进制 | 16 | 0123456789ABCDEF |
因为计算机系统中使用的是二进制数,所以这里仅对二进制数进行说明。
无符号二进制数的表示与其他进制数类似。二进制为遇2进1位,如3用二进制表示为11,计算机中按字节存储为00000011;7用二进制表示为00001111。
有符号二进制数中,正数的表示与无符号二进制数表示方法相同,负数通常采用补码的形式表示。负数的补码是将该数的无符号数值取反,然后末尾加1得到。如3的无符号数为00000011,对其取反得到11111100,然后再加1,得到11111101,即为-3的二进制表示。
2.2 非数值数据表示
非数值数据即指的是以字符为基础的各种其他类型数据存储,通常使用字符集进行编码存储。常见的字符集有ASCII、Unicode、GB2312等。ASCII采用7位二进制编码,最多可表示128个字符,所以适用于以英文字母为基础的数据存储。Unicode编码分为UTF-8、UTF-16、UTF-32等,可以容纳多个国家的不同语言的字符。GB2312是我国在ASCII的基础上进行扩展,对汉字进行编码的国家标准,采用16位编码。
2.3 基本数据单位
汇编中常见的数据单位如下表所示:
数据类型 | 单位 | 长度 |
字节(B) | Byte | 8bit |
字(W) | 2byte | 16bit |
双字(DW) | 4byte | 32bit |
四字(QW) | 8byte | 64bit |
十字节 | 10byte | 80bit |
字符串 | Char | — |
注:汇编中常会用到B、W、DW、QW等分别表示字节、字、双字、四字等。
2.4 数据的存储
数据的存储是指以二进制形式表示的数据和代码存放在存储器或者内存中。内存由一系列基本存储单元线性地组成,每一个存储单元有一个唯一的地址。通常,基本存储单元由8个连续的bit位构成,即以一个字节为单位存储数据。通常,CPU支持以多种形式表示存储单元的地址。
关于数据在内存中的存储有两种方式:
大端序(Big-endian):高位字节存入低地址,低位字节存入高地址。
小端序(Little-endian):低位字节存入低地址,高位字节存入高地址。
例如,将12345678h写入以1000h开始的内存中(h表示16进制),分别以Big-endian和Little-endian存放,对比如下:
存储方法 | 1000h | 1001h | 1002h | 1003h |
Big-endian | 12h | 34h | 56h | 78h |
Little-endian | 78h | 56h | 34h | 12h |
如下图所示:
#图来自《加密与解密(第四版)》,侵删
从上图可以看到,Big-endian的存储顺序与人类使用数据时的顺序一致,而Little-endian需要经过分析转换之后得到正确结果。一般x86系列CPU都是Little-endian字节序,网络协议使用Big-endian字节序,所以在汇编的使用过程中,Little-endian字节序使用的情况更多。
3.汇编语言介绍
3.1 机器指令
CPU能够直接识别并遵照执行的指令称为机器指令。机器指令一般由操作码(opcode)和操作数两部分组成,操作码指出要进行的操作或运算,操作数指出参与操作或运算的对象,也指出操作或运算结果存放的位置。机器指令采用二进制编码表示,其中有一部分表示操作码,另一部分表示操作数。
每一条机器指令的功能通常是很有限的,一条高级语言的语句所完成的功能,往往需要几条、几十条、甚至几百条机器指令才能够实现。
3.2 汇编语言及其分类
机器指令的操作码(opcode)难以识别和记忆,所以人们采用便于记忆、并能描述指令功能的符号来表示指令的操作码,这些符号被称为指令助记符。通过助记符进一步形成汇编格式指令,即汇编语言。汇编指令的一般格式为:[标号:] 指令助记符 [操作数表]
通常,汇编指令按功能可分为数据传送指令、算术逻辑运算指令、转移指令、处理器控制指令和其他指令等几个大类。
把用汇编语言编写的程序称为汇编语言源程序或汇编源程序。将汇编源程序翻译成目标程序的过程称为汇编,反之则称为反汇编。当然,现在常说的反汇编指的是从汇编语言到高级语言的翻译过程,个人认为是不够严谨的。
3.3 汇编语言优缺点
汇编语言的优点是利用它可编写出在“时空”两个方面最有效率的程序,并且可最直接和最有效的操纵机器硬件系统。
汇编语言的缺点是面向机器,与机器关系密切,它要求程序员比较熟悉机器硬件系统,要考虑许多细节问题,编写程序繁琐,调试、维护、移植困难。
3.4 汇编语言与二进制安全
在二进制安全中,汇编语言具有举足轻重的地位。无论是逆向工程还是系统漏洞挖掘,又或者是其他与二进制安全相关的工作,都存在一个很重要的前提就是精通汇编语言。而这个存在一个很重要的原因就是,众多的非开源程序是无法获得高级语言的源代码的,所以就要求二进制安全工作者从能够获得的汇编程序入手,去分析其中可能存在的潜在的不安全问题。
汇编语言的深入学习,能够有助于在二进制安全方面打好基础,从而促进二进制安全技术的研究与学习。
4.寄存器
利用寄存器存放运算数据和运算结果,效率是最高的。寄存器在CPU内部,处理寄存器中的数据要比处理存储器中的数据快很多,因此一般总是尽量利用寄存器。指令集中大部分指令的操作数据至少有一个在寄存器中,在目标代码中,绝大部分的指令都使用到寄存器。以IA-32为例,该系列CPU中的寄存器模型如下图所示:
#图来自网络,侵删
4.1 通用寄存器
IA-32系列的CPU中有8个32位的通用寄存器,分别为EAX、EBX、ECX、EDX、ESP、EBP、ESI和EDI。如上图所示,8个32位通用寄存器的低16位相当于8个16位的通用寄存器,可以单独存取它们。这些寄存器的名称分别为AX、BX、CX、DX、SP、BP、SI、DI,与16位的通用寄存器相对应,从而保持32位与16位相兼容。再进一步的,AX、BX、CX、DX四个16位寄存器可以进一步分为8个8位寄存器,即AH、AL、BH、BL、CH、CL、DH、DL等8个,但SP、BP、SI、DI不可再分。
这些寄存器不仅可以保存算术运算过程中的操作数,还可以作为指针给出存储单元的地址,或者给出计算存储单元地址过程中的一部分。因此,把这些寄存器称为通用寄存器。
8个寄存器除了通用的功能之外,每个都有各自的专门用途。EAX也称为累加器(Accumulator),函数返回值一般也存在eax中。EBX称为基(Base)地址寄存器。ECX称为计数(Counter)寄存器,在字符串操作和循环操作时,用它来控制重复循环操作次数,在移位操作时,CL 用于保存移位的位数。EDX称为数据(Data)寄存器。ESP是栈顶指针寄存器,其中存放栈顶地址。EBP是栈底指针寄存器,其中存放栈底指针。ESI是源变址寄存器和EDI是目的变址寄存器,其中一般存放指针地址,常用于将ESI和EDI所指向的内容进行比较等操作。
4.2 标志寄存器
标志寄存器中的标志位能够反映CPU运算处理后的某些状态,部分指令的执行会影响标志位,而且也有另一部分的指令执行是根据部分标志位来完成的。如上方图IA-32寄存器模型中所示,标志寄存器是EFLAGS字段,共32bit。具体标志位说明如下图所示:
#图来自网络,侵删
标志寄存器中的标志位分为状态标志和控制标志:
状态标志说明如下:
标志位 | 说明 |
CF | 进位标志,如果运算结果的最高位产生一个进位或错位,则CF置1,否则CF清0。 |
PF | 奇偶标志,如果运算结果低8位中“1”的个数为偶数时,则PF置1,否则PF清0。利用PF可以进行奇偶校验检查,或产生奇偶校验位。 |
AF | 辅助进位标志,反应运算结果低四位产生进位或错位的情况。 |
ZF | 零标志,反应UN算结果是否为0,如果运算结果为0,则ZF置1,否则为0。 |
SF | 符号标志,SF与运算结果的最高位相同,如果运算结果为负,即一个数的最高位为1,则SF置1,否则SF清零。 |
OF | 溢出标志,反映有符号数加减运算是否引起溢出,若运算结果超出补码表示范围(8位-128~+127,16位-32768~+32767),若溢出,则OF置1,否则OF为0。 |
控制标志说明如下:
标志位 | 说明 |
TF | 单步标志位,用于程序跟踪调试。当TF=1,CPU进入单步方式。 |
IF | 中断允许位,当IF=1时,CPU为开中断,当IF=0时,CPU为关中断。 |
DF | 方向位,决定串操作指令执行时的指针寄存器的调整方向。 |
4.3 段寄存器
段寄存器支持以分段方式管理存储器。在32位CPU中有6个段寄存器,分别是CS(代码段寄存器)、SS(堆栈段寄存器)、DS(数据段寄存器)、ES(附加段寄存器)以及FS和GS。其中FS和GS也是附加段寄存器,是32位CPU在原16位的基础上扩展而来。段寄存器的长度都是16位,在实地址方式下存放16位的段值,在保护方式下,存放16位的段选择子。
4.4 逻辑地址
在采用分段存储管理方式之后,程序中使用的某个存储单元总是属于某个段。在一个已确定的段内,只需要通过偏移便可指定要访问的存储单元,于是可以用段号和段内地址来表示存储单元,所以程序中绝大部分涉及存储器访问的指令都只给出偏移(段号存放在段寄存器中)。
逻辑地址有两种表示方式:
段号:段内地址
段号:偏移
段内地址表示该存储单元所在段内的编号,如小区内某个住户的门牌号。偏移指的是存储单元的物理地址与所在段起始地址的差值,即为段内偏移,简称偏移。所以在知道偏移的情况下就可以获得相应存储单元的物理地址(因为段起始地址是已知的),即物理地址=段起始地址+偏移。
5.基础汇编指令
一般常见的汇编指令语法格式有Intel和AT&T。在 Intel语法中,第一个操作数是目的操作数,第二个操作数源操作数。而在 AT&T 中,第一个数是源操作数,第二个数是目的操作数。两者格式如下(以mov指令为例):
汇编 | 指令格式举例 | 说明 |
Intel | MOV DEST,SRC | 将SRC传送到DEST中,目标操作数在源操作数的左边 |
AT&T | MOV SRC,DEST | 将SRC传送到DEST中,目标操作数在源操作数的右边 |
在Intel的汇编语言语法中,寄存器和和立即数都没有前缀。但是在 AT&T 中,寄存器前冠以“%”,而立即数前冠以“$”。在 Intel的语法中,十六进制和二进制立即数后缀分别冠以“h”和“b”,而在 AT&T 中,十六进制立即数前冠以“0x”:
Intel 语法 | AT&T 语法 |
Mov eax,8 | movl $8,%eax |
Mov ebx,0ffffh | movl $0xffff,%ebx |
int 80h | int $0x80 |
以下将基于Intel汇编指令格式进行阐述。
一条汇编指令有四个组成部分,即标号(可选)、指令助记符(必需)、操作数(通常是必需的)、注释(可选)等,指令助记符(即汇编指令)是绝对不可缺少的,操作数一般可以有一个或两个。
常见的基础汇编指令如下:
指令及其格式 | 说明 |
MOV DEST,SRC | 传送指令,将SRC传递给DEST。SRC可以是通用寄存器、存储单元或立即数,DEST可以是通用寄存器或存储单元。 |
ADD DEST,SRC | 加法指令,将DEST和SRC相加,结果存入DEST。SRC可以是通用寄存器、存储单元或立即数,DEST可以是通用寄存器或存储单元。 |
SUB DEST,SRC | 减法指令,DEST减去SRC,结果存入DEST。SRC可以是通用寄存器、存储单元或立即数,DEST可以是通用寄存器或存储单元。 |
INC DEST | 自加1指令,DEST加1,结果存入DEST。DEST可以是通用寄存器或存储单元。 |
DEC DEST | 自减1指令,DEST减1,结果存入DEST。DEST可以是通用寄存器或存储单元。 |
NEG OPRD | 补运算指令,用0减去OPRD后,结果存入OPRD。得到的结果是原OPRD的相反数,OPRD以补码形式表示。 |
XCHG OPRD1,OPRD2 | 交换指令,将OPRD1的内容与OPRD2的内容交换。OPRD1和OPRD2可以是通用寄存器或存储单元,但不能同时是存储单元,也不能有立即数。 |
PUSH SRC | 圧栈指令,将SRC压入堆栈。SRC可以是通用寄存器、段寄存器、存储单元或立即数。 |
POP DEST | 出栈指令,将栈顶弹出的数据存放到DEST中。所弹出的数据长度与DEST的存储长度等长,DEST可以是通用寄存器、段寄存器或存储单元。 |
LEA REG,OPRD | 取址指令,将OPRD的有效地址传送给REG。OPRD必须是一个存储器操作数,REG必须是一个通用寄存器。 |
CMP DEST,SRC | 比较指令,用DEST减去SRC的差来影响标志寄存器中的标志位,但不将差值保存。CMP指令除了不保存结果外,与SUB指令相同。 |
ADC DEST,SRC | 带进位加法指令,DEST和SRC相加,再加上CF位,将结果存入DEST中。其他类似于ADD指令。 |
SBB DEST,SRC | 带借位减法指令,DEST减去SRC,再减去CF位,将结果存入DEST中。其他类似于SUB指令。 |
6.寻址方式
汇编语言中的寻址方式主要有立即寻址、寄存器寻址、直接寻址、寄存器间接寻址、寄存器相对寻址、基址加变址寻址、相对基址加变址寻址等七种,其中除了立即寻址和直接寻址外,其他5中方式的操作数都是存放在存储器中的,统称为存储器寻址。最后,在6.8 小节中的按地址寻址主要讲述lea命令的特殊使用。
6.1 立即寻址
立即寻址方式的操作数直接作为指令的一部分,即直接将一个数赋值给存储器,这个数也称为立即数。如下所示:
将0x12121212H赋值给EAX寄存器:
MOV EAX,12121212H
对EBX加上0x32141278H:
ADD EBX,32141278H
立即数寻址只能是将立即数作为源操作数,不能为目的操作数。
6.2 寄存器寻址
寄存器寻址时,操作数存放在寄存器中,如下所示:
将EAX中的值放到EDX中:
MOV EDX,EAX
交换两个寄存器中的值:
XCHG AL,BL
将DS的段地址存到BX中:
MOV BX,DS
注意,DS、ES段等都可以作为目的操作数,但CS不能作为目的操作数,不能显式的改变代码段寄存器CS。而且,不能直接将立即数传送到段寄存器中。
6.3 32位汇编存储器寻址方式通用表示
#图来自网络,侵删
32位汇编中存储器寻址的通用表示如上图所示,8个寄存器均可作为基址寄存器,除ESP外,其他7个寄存器均可作为变址寄存器。存储器操作的尺寸可以是字节、字或双字。存在变址寄存器时,变址寄存器中的值乘以1、2、4或8的比例因子之后,再进行有效地址的计算。具体举例详见6.4~6.8。
6.4 直接寻址
寄存器直接寻址时,操作数在存储器中,指令中包含的是待处理数据的存储单元的有效地址(EffectiveAddress,EA,有效地址指的是待访问存储单元的段内偏移地址)。此时只有基址寄存器,没有变址寄存器和位移量。例如:
在0xABCDEF12H的地址中存放的数据是0x12343421H,若将数据移到EAX中,使用如下操作:
MOV EAX,[ABCDEF12H]
注意,使用"[]"括起来的内容是一个地址,这个地址带上“[]”后表示该取地址中的数据。
将EBX中的值传送给特定地址:
MOV [ABCDEF12H],EBX
在直接寻址中,如果操作数的的地址不是默认存放在DS中,而是存放在ES或SS段中时,需要在指令中指明段超越。如将ES段的数据存放到EDX中:
MOV EDX,ES:[1234ABCDH]
6.5 寄存器间接寻址
在寄存器间接寻址方式中,操作数放置在存储器中,操作数的偏移地址放置在EBX、ESI、EDI、EBP等基址或变址寄存器中。若在EBX、ESI、EDI中时,数据存储在DS段中,若在EBP中,数据存储在SS段。此时只有基址寄存器,没有变址寄存器和位移量。使用方式如下:
MOV EAX,[EDI]
MOV [EBX],EAX
6.6 寄存器相对寻址
寄存器相对寻址的操作数同样在存储器中,操作数的偏移地址并不是直接存储在EBX、ESI、EDI、EBP等基址或变址寄存器中,而是通过这些寄存器所指向的偏移地址加上或减去一个位移量。这个位移量可以是8位、16位或32位的位移量之和,采用补码的形式表示,在计算有效地址时,如果位移量是8位或16位的,将会被带符号扩展成32位。此时存在基址寄存器和位移量,没有变址寄存器。如下所示:
MOV [EBX+12H]
6.7 基址加变址寻址
基址加变址寻址中,存在基址寄存器和变址寄存器,没有位移量。如下所示:
MOV EAX,[EBX+ESI]
MOV EAX,[ECX+EBX*4]
注意:变址寄存器所乘的倍率是与变址寄存器中的值相乘,而不是与变址寄存器的地址相乘。
6.8 相对基址加变址寻址
在相对基址加变址寻址方式中,存在基址寄存器、变址寄存器和位移量。如下所示:
MOV EAX,[EDI+EBP*4-12H]
6.9 取有效地址指令
32位汇编中使用LEA命令专门来取得有效地址,在一定程度上提高效率。格式如下:
LEA REG,OPRD
该指令将OPRD的有效地址传送到REG,OPRD必须是一个存储器操作数,REG必须是一个16位或32位的通用寄存器。LEA命令不影响标志寄存器中的标志位。例如:
假设12345678H是一个有效地址,该地址加6H的地方(1234567EH)中存储的数据是666(十六进制为0x29AH),以下来说明MOV和LEA的区别:
MOV EDI,12345678H
MOV EAX,[EDI+6H]
LEA EBX,[EDI+6H]
此时EAX存放的是29AH,EBX中存放的是1234567EH。
参考资料:
-
《新概念汇编语言》,杨季文,2017
-
《逆向工程权威指南》
-
https://blog.youkuaiyun.com/weixin_30895603/article/details/97641349
-
《加密与解密(第4版)》
-
https://blog.youkuaiyun.com/u010481185/article/details/8949844
-
https://www.cnblogs.com/xiaojianliu/articles/8733496.html
-
http://c.biancheng.net/view/3468.html
博客与公众号同步发文,欢迎关注: