数制/进制(第2课)

进制也就是进位制,是人们规定的一种进位方法。 对于任何一种进制—X进制,就表示某一位置上的数运算时是逢X进一位。 十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一,以此类推,x进制就是逢x进位。

 

1、二进制

二进制数有两个特点:它由两个基本数字0,1组成,二进制数运算规律是逢二进一。

为区别于其它进制数,二进制数的书写通常在数的右下方注上基数2,或加后面加B表示。

例如:二进制数10110011可以写成(10110011)2,或写成10110011B

 

2、八进制

八进制的基R=8=2^3,有数码0、1、2、3、4、5、6、7,并且每个数码正好对应三位二进制数,所以八进制能很好地反映二进制。八进制用下标8或数据后面加O表示 例如:二进制数据 ( 11 101 010 . 010 110 100 )2 对应 八进制数据 ( 3 5 2 . 2 6 4 )8或352.264O.

 

3、十六进制

十六进制数有两个基本特点:它由十六个字符0~9以及A,B,C,D,E,F组成(它们分别表示十进制数10~15),十六进制数运算规律是逢十六进一,即基R=16=2^4,通常在表示时用尾部标志H或下标16以示区别。

例如:十六进制数4AC8可写成(4AC8)16,或写成4AC8H。

 

4、进制转换

说到进制转换,不得不提一下位权。对于形式化的进制表示,我们可以从0开始,对数字的各个数位进行编号,即个位起往左依次为编号0,1,2,……;对称的,从小数点后的数位则是-1,-2,……

 

进行进制转换时,我们不妨设源进制(转换前所用进制)的基为R1,目标进制(转换后所用进制)的基为R2,原数值的表示按数位为AnA(n-1)……A2A1A0.A-1A-2……,R1在R2中的表示为R,则有(AnA(n-1)……A2A1A0.A-1A-2……)R1=(AnR^n+A(n-1)R^(n-1)+……+A2R^2+A1R^1+A0R^0+A-1R^(-1)+A-2*R^(-2))R2

 

二进制数、十六进制数转换为十进制数的规律是相同的。把二进制数(或十六进制数)按位权形式展开多项式和的形式,求其最后的和,就是其对应的十进制数——简称“按权求和”.

 

例如:

把(1001.01)2 二进制计算。

解:(1001.01)2

=81+40+20+11+0(1/2)+1(1/4)

=8+0+0+1+0+0.25

=9.25

 

把(38A.11)16转换为十进制数

解:(38A.11)16

=3×16的2次方+8×16的1次方+10×16的0次方+1×16的-1次方+1×16的-2次方

=768+128+10+0.0625+0.0039

=906.0664

 

十进制数转换为二进制数,十六进制数(除2/16取余法)

整数转换.一个十进制整数转换为二进制整数通常采用除二取余法,即用2连续除十进制数,直到商为0,逆序排列余数即可得到――简称除二取余法.

例:将25转换为二进制数

解:25÷2=12 余数1

12÷2=6 余数0

6÷2=3 余数0

3÷2=1 余数1

1÷2=0 余数1

所以25=(11001)2

 

同理,把十进制数转换为十六进制数时,将基数2转换成16就可以了.

例:将25转换为十六进制数

解:25÷16=1 余数9

1÷16=0 余数1

所以25=(19)16

 

二进制数与十六进制数之间的转换

由于4位二进制数恰好有16个组合状态,即1位十六进制数与4位二进制数是一一对应的.所以,十六进制数与二进制数的转换是十分简单的.

 

(1)十六进制数转换成二进制数,只要将每一位十六进制数用对应的4位二进制数替代即可――简称位分四位.

例:将(4AF8B)16转换为二进制数.

解: 4 A F 8 B

0100 1010 1111 1000 1011

所以(4AF8B)16=(1001010111110001011)2

 

(2)二进制数转换为十六进制数,分别向左,向右每四位一组,依次写出每组4位二进制数所对应的十六进制数――简称四位合一位.

例:将二进制数(000111010110)2转换为十六进制数.

解: 0001 1101 0110

1 D 6

所以(111010110)2=(1D6)16

转换时注意最后一组不足4位时必须加0补齐4位

 

数制转换的一般化

1)R进制转换成十进制

任意R进制数据按权展开、相加即可得十进制数据。例如:N = 1101.0101B = 12^3+12^2+02^1+12^0+02^-1+12^-2+02^-3+12^-4 = 8+4+0+1+0+0.25+0+0.0625 = 13.3125

N = 5A.8H = 516^1+A16^0+8*16^-1 = 80+10+0.5 = 90.5

 

2)十进制转换R 进制

十进制数转换成R 进制数,须将整数部分和小数部分分别转换.

1.整数转换———除R 取余法 规则:

(1)用R 去除给出的十进制数的整数部分,取其余数作为转换后的R 进制数据的整数部分最低位数字;

(2)再用R去除所得的商,取其余数作为转换后的R 进制数据的高一位数字;

(3)重复执行(2)操作,一直到商为0结束。

例如:115 转换成 Binary数据和Hexadecimal数据 (图2-4) 所以 115 = 1110011 B = 73 H

 

2.小数转换—————乘R取整法规则:

(1)用R去乘给出的十进制数的小数部分,取乘积的整数部分作为转换后R 进制小数点后第一位数字;

(2)再用R 去乘上一步乘积的小数部分,然后取新乘积的整数部分作为转换后R 进制小数的低一位数字;

(3)重复(2)操作,一直到乘积为0,或已得到要求精度数位为止。

 

第三章:语言

 

3.1逻辑运算

逻辑运算又称为布尔运算。通常用来测试真假值,最常见到的逻辑运算就是循环的处理,用来判断是否该离开循环或继续执行循环内的指令。

逻辑常量只有两个,0和1,用来表示两个对立的逻辑状态。在逻辑代数中,有与、或、非三种基本的逻辑运算。

 

与运算(AND)

“与”运算是一种二元运算,它定义了两个变量A和B的一种函数关系。用语句来描述它,这就是:当且仅当变量A和B都为1时,函数F为1。

 

下表是与运算的真值表:

AND 1 0
1 1 0
0 0 0

 

或运算(OR)

“或”运算是另一种二元运算,它定义了变量A、B与函数F的另一种关系。用语句来描述它,这就是:只要变量A和B中任何一个为1,则函数F为1。

 

下表是或运算的真值表:

OR 1 0
1 1 1
0 1 0

 

非运算(NOT)

逻辑“非”运算是一元运算,它定义了一个变量(记为A)的函数关系。用语句来描述之,这就是:当A=1时,则函数F=0;反之,当A=0时,则函数F=1

 

下表是非运算的真值表:

NOT 1 0
0 1

 

算术左移、逻辑左移

左移用来将一个数的各位二进制位全部左移若干位。例如:

将a的二进制数左移2位,右补0。若a=15,即二进制数00001111,左移2位得00111100,即十进制数60,高位左移后溢出,舍弃。左移一位相当于该数乘以2,左移2位相当于该数乘以2^2=4。上面举的例子15<< 2=60,即乘了4。但此结论只适用于该数左移时被溢出舍弃的高位中不包含1的情况。

 

循环左移

循环左移类似于逻辑左移,不同的是,循环左移会将左边移出的位添补到左边。例如原数x3x2x1x0,循环左移一位后,变为x2x1x0 x3。

可见,所有的位顺序向左移1位,最低位由最高位循环移入。

 

带进位的循环左移

此方法和循环左移类似,只是多了一个符号位,举例来说:

原数:CX3X2X1X0,循环左移一位后变为X3X2X1X0C。

 

算术右移

算术右移用来将一个数的各位二进制位全部右移若干位,然后在左侧用原符号位补齐。在汇编语言中,如果最高位为1,则补1,否则补0。如将10000000算术右移7位,应该变成11111111。

 

逻辑右移

逻辑右移是将各位依次右移指定位数,然后在左侧补0.不考虑符号位。例如将10000000逻辑右移7位,变为00000001。

 

循环右移

循环右移类似于逻辑右移,不同的是,循环右移会将右边移出的位添补到左边。例如原数x3x2x1x0,循环右移一位后,变为x0x3x2x1。

可见,所有的位顺序向右移1位,最高位由最低位循环移入。

 

带进位的循环右移

此方法和循环右移类似,只是多了一个符号位,举例来说:

原数:CX3X2X1X0,循环右移一位后变为X0CX3X2X1。

 

3.2 流程控制语句的识别

流程控制语句的识别时进行逆向分析和还原高级代码的基础,详细的理解此基础可以更好的理解高级语言中流程控制的内部实现机制,对开发和调试大有益处。

 

If语句

If语句是分支结构的重要组成部分。If语句的功能是现对运算条件进行比较,然后根据比较结果选择对应的语句块来执行。If语句只能判断两种情况:0为假值,非0为真值。如果为真值,则进入语句块内执行语句;如果为假值,则跳过if语句块,继续执行程序的其他语句。要注意的是,if语句转换的条件跳转指令与if语句的判断结果是相反的。

 

If语句的一般流程如下:

//先执行各类影响标志位的指令

//其后是各种条件跳转指令

jxx xxxx

 

if…else…语句

if语句是一个单分支结构,if…else…组合后是一个双分支结构。两者完成的功能有所不同。从语法上看,if…else…只比if语句多出了一个else。else有两个功能,如果if判断成功,则跳过else分支语句块;如果if判断失败,则进入else分支语句块中。有了else语句的存在,程序在进行流程选择时,必会经过两个分支中的一个。

 

if…else…的大致流程如下:

先执行影响标志位的相关指令

jxx else_begin //该地址为else语句块的首地址

if_begin

……. //if语句块内的执行代码

if_end

jmp else_end //跳转到else语句块的结束地址

else_begin

…… //else语句块内的执行代码

else_end

 

如果遇到以上指令序列,先考察其中的两个跳转指令,当第一个条件跳转指令跳转到地址else_begin处之前有个jmp指令,则可将其视为由if…else…组合而成的双分支结构。根据这两个跳转指令可以得到if和else语句块的代码边界。通过cmp和jxx可还原出if的比较信息,jmp指令之后即为else块的开始。

 

if构成的多分支结构:

多分支结构类似于if…else…的组合方式,在if…else…的else之后再添加一个else if进行二次比较,这样就可以进行多次比较,再次选择程序流程,形成多分支流程。它的c++语法格式为:if…else if…else if…,可重复后缀为else if。当最后为else时,便到了多分支结构的末尾处。

 

一般流程如下:

jxx指出了下一个else if的起始点,而jmp指出了整个多分支结构的末尾地址以及当前if或者else if语句块的末尾。最后的else块的边界也很容易识别,如果发现多分支块内的某一段代码在执行前没有判定,即可定义为else块。

//会影响标志位的指令

jxx else_if_begin //跳到下一条else if语句块的首地址

if_begin

…… //if语句块内的执行代码

if_end

jmp end //跳转到多分枝结构的结尾地址

else_if_begin //else if语句块的起始地址

//可影响标志位的指令

jxx else_begin

……

else_if_end:

jmp end

else_begin:

……

end

……

 

当每个条件跳转指令的跳转地址之前都紧跟jmp指令,并且他们的跳转地址值都一样时,可视为一个多分支结构。

 

switch:

switch是比较常用的多分支结构,使用起来也非常方便,并且效率也高于if…else if多分枝结构。switch语句将所有的条件跳转都放置在了一起,并没有发现case语句块的踪影,通过条件跳转指令,跳转到相应的case语句块中。因此每个case的执行是由switch比较结果引导跳过来的。

 

一般流程如下:

mov reg,mem //取出switch中考察的变量

//影响标志位的指令

jxx xxxx

//影响标志位的指令

jxx xxxx

//影响标志位的指令

jxx xxxx

jmp end //跳到switch语句块的结尾地址出

…… //case语句块首地址

jmp end //跳到switch语句块的结尾地址出

…… //case语句块首地址

jmp end //跳到switch语句块的结尾地址出

…… //case语句块首地址

jmp end //跳到switch语句块的结尾地址出

end:

……

 

当分支数小于4的情况下,VC 6.0会采取模拟if else的方法

 

当分支数大于3,并且case的判定值存在明显线性关系组合时,它会制作一份case地址数组(case地址表),这个数组保存了每个case语句块的首地址,并且数组下标以0起始。如果每两个case值之间的差值小于等于6,并且case语句数大于等于4,编译器就会形成这种线性结构。

 

对于非线性的switch结构,会进行索引表优化,需要两张表:一张为case语句块地址表,另一张为case语句块索引表

 

地址表中的每一项保存了一个case语句块的首地址,有几个case语句就有几项。此情况适用于差值小于等于255的情况,大于255的话可以通过树方式优化。

 

do循环:

do循环的工作流程清晰,识别起来也相对简单。根据其特性,先执行语句块,再进行比较判断,当条件成立时,会继续执行语句块。

if语句的比较是相反的,并且跳转地址大于当前代码的地址,是一个向下跳转的过程;而do中的跳转地址小于当前代码的地址,是一个向上跳转的过程,所以条件跳转的逻辑与源码中的逻辑相同

 

do循环的一般流程:

do_begin

……. //循环语句块

;影响标记位的指令

jxx do_begin

 

while循环

while循环和do循环正好相反,在执行循环语句块之前,必须要进行条件判断,根据比较结果再选择是否执行循环语句块。识别while循环,查看条件跳转地址,如果这个地址上面有一个jmp指令,并且此指令跳转到的地址小于当前代码地址,那么明显是一个向上跳转的地址。要完成语句循环,就需要修改程序流程,回到循环语句处,因此向上跳转就成了循环语句的明显特征。在条件跳转的地址附近会有jmp指令修改程序流程。

while循环用了两次跳转,因此比do循环效率低一些。

 

while循环的一般流程:

while_begin

;影响标记位的指令

jxx while_end

……

jmp while_begin

while_end:

 

for循环:

for循环是三种循环结构中最复杂的一种。for循环由赋初值,设置循环条件,设置循环步长这三条语句组成。由于for循环更符合人类的思维方式,在循环结构中被使用的频率也很高。

 

for循环的一般流程:

mov mem/reg ,xxx //赋初值

jmp for_cmp //跳到循环条件判定部分

for_step:

//修改循环变量step

mov reg,step

add reg,xxxx //修改循环变量的计算过程,在实际分析中,视算法不同而不同

mov step,eax

for_cmp: //循环条件判定部分

mov ecx,dword ptr step

//判定循环变量和循环终止条件stepend的关系,满足条件则退出for循环

cmp ecx,stepend

jxx for_end //条件成立则结束循环

…….

jmp for_step //向上跳转,修改流程回到步长计算部分

for_end:

 

在计数器变量被赋初值后,利用jmp跳过第一次步长计算,然后,可以通过三个跳转指令还原for循环的各个组成部分:第一个jmp指令之前的代码为初始化部分;从第一个jmp指令到循环条件比较处(也就是上面代码中for_cmp标号的位置)之间的代码为步长计算部分;在条件跳转指令jxx之后寻找一个jmp指令,这jmp指令必须是向上跳转的,且其目标是到步长计算的位置,在jxx和这个jmp(也就是上面代码的省略号所在的位置)之间的代码是循环语句块。

 

3.3 栈

从数据结构角度看,栈是一种用来存储数据的容器。放入数据的操作称为压入(push),从栈中取出数据的操作被称为弹出(pop)。存取数据的一条基本规则是后进先出。

X86架构有对栈的内建支持。用于这种支持的寄存器包括esp和ebp。其中esp是栈指针,包含了指向栈顶的内存地址。当数据被压入或弹出栈时,这个寄存器的值相应的改变。Ebp是栈基址寄存器,在一个函数中保持不变,因此程序把它当成定位器,用来确定局部变量和参数的位置。

与栈有关的指令包括push,pop,call,leave,enter和ret。在内存中,栈被分配成自顶向下的,最高的地址最先被使用。当一个值被压入栈时,使用低一点的地址。

栈只能用于短期存储。他经常用于保存局部变量、参数和返回地址。主要用途是管理函数调用之间的数据交换。不同的编译器对这种管理方法的具体实现有所不同,但大部分常见约定都使用ebp的地址来引用局部变量与参数。

 

栈的布局:

在经典的操作系统中,栈总是向下(低地址)增长的。

栈保存一个函数调用所需要的维护信息,常被称为堆栈帧或者是活动记录,堆栈帧一般包括:

(1)函数的返回地址和参数;

(2)临时变量:包括函数的非静态局部变量以及编译器生成的其他局部变量;

(3)保存的上下文:包括在函数调用前后保持不变的寄存器。

 

3.4 堆

堆是组织内存的另一种重要方法,是程序运行期动态申请内存空间的主要途径。与栈空间是由编译器产生的代码自动分配和释放不同,堆上的空间需要程序员自己写代码来申请(HeapAlloc)和释放(HeapFree),而且分配和释放操作应该严格匹配,忘记释放或多次释放都是不正确的。

 

3.5异常

异常通常是CPU在执行指令是因为检测到预先定义的某个(或多个)条件而产生的同步事件,异常的来源有3种,第一种是程序错误,即当CPU在执行程序指令时遇到操作数有错误(执行除法指令时遇到除数是0)或检测到指令规范中定义的非法情况(用户模式下执行特权指令等)。第二种来源是某些特殊指令,这些指令的预期行为就是产生相应的异常,比如INT3指令,该指令的目的就是产生一个断点异常,让CPU中断进调试器。第三种来源是奔腾CPU引入的机器检查异常,即当CPU执行指令期间检测到CPU内部或外部的硬件错误。

 

异常分为3类,错误,陷阱和中止

错误类异常

导致错误类异常的情况通常可以被纠正,而且一旦纠正后,程序可以无损失的恢复执行。此类异常的一个最常见的例子就是内存页错误。页错误异常的发生是因为它是虚拟内存的基础。因为物理内存的空间有限,所以操作系统会把某些在那时不用的内存以页为单位交换到外部存储器上。当有程序访问到这些不在物理内存种的页所对应的内存地址时,CPU便会产生一个页错误异常(缺页错误、缺页异常),并转去执行该异常的处理程序,后者会调用内存管理器的函数把对应的内存页交换回物理内存,然后再让CPU返回到导致该异常的那条指令处恢复执行。当第二次执行刚才导致异常的指令时,对应的内存页已经在物理内存中(错误情况被纠正),因此就不会再产生页错误异常了。

当CPU报告错误类异常时,CPU将其状态恢复成导致该异常的指令被执行之前的状态。而且在CPU转去执行异常处理程序前,在栈中保存的CS和EIP指针是指向导致异常的这条指令的(而不是下一条指令)。因此,当异常处理程序返回继续执行时,CPU接下来执行的第一条指令仍然是刚才导致异常的那条指令。所以,如果导致异常的情况还没有被消除,那么CPU会再次产生异常。

 

陷阱类异常

当CPU报告陷阱类异常时,导致该异常的指令已经执行完毕,压入栈的CS和EIP值是导致该异常的指令执行后紧接着要执行的下一条指令。值得说明的是,下一条指令并不一定是与导致异常的指令相邻的下一条。如果导致异常的指令是跳转指令或函数调用指令,那么下一条指令可能是内存地址不相邻的另一条指令。

导致陷阱类异常的情况通常也是可以无损失的恢复执行的。比如INT 3指令导致的断点异常就属于陷阱类异常,该异常会使CPU中断到调试器,从调试器返回后,被调试程序可以继续执行。

 

中止类异常

中止类异常主要用来报告严重的错误,比如硬件错误和系统表中包含非法值或不一致的状态等。这类异常不允许恢复继续执行。首先,当这类异常发生时,CPU并不总能保证报告的异常的指令地址是精确地。另外,出于安全性的考虑,这类异常可能是由于导致该异常的程序执行非法操作导致的,因此就应该强迫其中止退出。

 

3.6 中断/异常处理

中断和异常从产生的根源来看有着本质的区别,但是系统(CPU和操作系统)是用统一的方式来响应和管理他们的。中断和异常处理的核心数据结构是中断描述符表(IDT)。当中断和异常发生时,CPU通过查找IDT表来定位处理例程的地址,然后转去执行该处理例程。这个查找的过程是在CPU内部执行的。通常,系统软件(操作系统和BIOS固件)在系统初始化阶段就准备好中断处理例程和IDT表,然后把IDT表的位置通过IDTR寄存器告诉CPU。

实模式下IVT(中断向量表)位于物理地址0开始的1KB内存区中,每个IVT表项的长度是4个字节,共有256个表项,与x86CPU的256个中断向量一一对应。实模式下,每个IVT表项的资格字节分为两部分,高两个字节为中断例程的段地址,低两个字节为中断例程的偏移地址。因为是在实模式下,所以段地址左移4位再加上偏移地址便可以得到20位的中断例程地址。

 

下面是IA-32 CPU相应中断和异常的全过程:

  1. 将代码段寄存器CS和指令指针寄存器(EIP)的低16位压入堆栈
  2. 将标志寄存器EFLAGS的低16位压入堆栈
  3. 清除标志寄存器的IF标志,以禁止其他中断
  4. 清除标志寄存器的TF,RF,AC标志
  5. 使用向量号n作为索引,在IVT中找到对应的表项(n*4+IVT表基地址)
  6. 将表项中的段地址和偏移地址分别装入CS和EIP寄存器中,并开始执行对应的代码
  7. 中断例程总是以IRET指令结束。IRET指令会从堆栈中弹出前面保存的CS,IP和标志寄存器值,然后返回执行被中断的程序。

 

3.7 中断

中断通常是由CPU外部的输入输出设备(硬件)所触发的,供外部设备通知CPU“有事情要处理”,因此又叫中断请求。中断请求的目的是希望CPU暂时停止执行 当前正在执行的程序,转去执行中断请求所对应的中断处理例程。

中断机制为CPU和外部设备间的通信提供了一种高效的方法,有了中断机制,CPU就可以不用去频繁的查询外部设备的状态了,因为外部设备有事需要处理时,他可以发出中断请求通知CPU。

在硬件级,中断是由一块专门芯片来管理的,通常称为中断控制器。他负责分配中断资源和管理各个中断源发出的中断请求。为了便于标识各个中断请求,中断管理器通常用IRQ后面加上数字来表示不同路的中断请求信号。

 

3.8 函数调用约定

 

cdecl
cdecl调用约定又称为C调用约定,是c/c++语言缺省的调用约定。参数按照从右至左的方式入栈,函数本身不清理栈,此工作有调用者负责,返回值在eax中。由于由调用者清理栈,所以允许可变参数函数存在。

 

stdcall

stdcall很多时候被称为pascal调用约定。pascal语言是早期很常见的一种教学用计算机程序设计语言,其语法严谨,参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在eax中。

 

fastcall

fastcall的调用方式运行相对快,因为它通过寄存器来传递参数。它使用ecx和edx传送两个双字或更小的参数,剩下的参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在eax中。

 

naked

naked是一个很少见的调用约定,一般不建议使用。编译器不会给这种函数增加初始化的清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果,此调用约定必须跟declspec同时使用,例如声明一个函数,如_declspec(naked) int add(int a,int b);

 

pascal

这是pascal语言的调用约定,跟stdcall一样,参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在 eax中,vc已经废弃了这种调用方式,因此在写vc程序时,建议使用stdcall。

 

thiscall

这是c++语言特有的一种调用方式,用于类成员函数的调用约定。如果参数确定,this指针存放于ecx寄存器,函数自身清理堆栈;如果参数不确定,this指针在所有参数入栈后再入栈,调用者清理栈。Thiscall不是关键字,程序员不能使用。参数按照从右至左的方式r入栈。

 

 

头像
  • ¥ 199.0元
  • 市场价:399.0元
  • ¥ 69.0元
  • 市场价:99.0元
  • ¥ 498.0元
  • 市场价:498.0元
  • ¥ 58元
  • 市场价:58元

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: