5.0伪代码
伪代码是给处理器的指令,它实际上是原始十六进制代码的可读版。因此,汇编是最低级的编程语言。汇编中的所有东西被直接翻译为十六进制码。换句话说,你没有把高级语言翻译为低级语言的编译器上的烦恼,汇编器仅仅把汇编代码转化为原始数据。
本章将讨论一些用来运算,位操作等的伪代码。还有跳转指令,比较等伪代码在后面介绍。
3. 1一些基本的计算伪代码
MOV
这条指令用来把一个地方移往(事实上是复制到)另一个地方。这个地方可以是寄存器,内存地址或是直接数值(当然只能作为源值)。Mov指令的语法是:
mov 目标,源
你可把一个寄存器移往另一个(注意指令是在复制那个值到目标中,尽管“mov”这个名字是移的意思)
mov edx, ecx
上面的这条指令把ecx的内容复制到了ecx中,源和目标的大小应该一致。例如这个指令是非法的:
mov al, ecx;非法
这条伪代码试图把一个DWORD(32位)值装入一个字节(8位)的寄存器中。这不能个由mov指令来完成(有其他的指令干这事)。但这些指令是允许的因为源和目标在大小上并没有什么不同:
mov al, bl
mov cl, dl
mov cx, dx
mov ecx, ebx
内存地址由offset指示(在win32中,前一章中有更多信息)你也能从地址的某一个地方获得一个值并把它放入一个寄存器中。下面有一个例子:
offset | 34 | 35 | 36 | 37 | 38 | 39 | 3A | 3B | 3C | 3D | 3E | 3F | 40 | 41 | 42 |
data | 0D | 0A | 50 | 32 | 44 | 57 | 25 | 7A | 5E | 72 | EF | 7D | FF | AD | C7 |
每一个块代表一个字节
offset的值这里是用字节的形式表示的,但它事实上是32位的值,比如3A(这不是一个常见的offset的值,但如果不这样简写表格装不下),这也是一个32位的值:0000003Ah。只是为了节省空间,使用了一些不常见的低位offset。所有的值均为16进制。
看上表的offset 3A。那个offset的数据是25, 7A, 5E, 72, EF等。例如,要把这个位于3A的值用mov放入寄存器中:
mov eax, dword ptr[0000003Ah]
(h后缀表明这是一个十六进制值)
mov eax, dword ptr[0000003Ah]这条指令的意思是:把位于内存地址3A的DWORD大小的值放入eax寄存器。执行了这条指令后,eax包含了值725E7A25h。可能你注意到了这是在内存中时的反转结果:25 7A 5E 72。这是因为存储在内存中的值使用了little endian格式。这意味着越靠右的字节位数越高:字节顺序被反转了。我想一些例子可以使你把这个搞清楚。
十六进制dword(32位)值放在内存中时是这样:40, 30, 20, 10(每个值占一个字节(8位))
十六进制word(16位)值放在内存中时是这样:50, 40
回到前面的例子。你也可以对其他大小的值这么做:
mov cl, byte ptr [34h] ; cl得到值0Dh(参考上表)
mov dx, word ptr [3Eh] ; dx将得到值 7DEFh (看上表,记住反序)
大小有时不是必须的。
Mov eax,[00403045h]
因为eax是32位寄存器,编译器假定(也只能这么做)它应该从地址403045(十六进制)取个32位的值。
直接数值是允许的:
mov edx, 5006
这只是使得edx寄存器装有值5006,综括号[和]用来从括号间的内存地址处取值,没有括号就只是这个值。寄存器和内存地址也可以(他应该是32位程序中的32位寄存器):
mov eax,403045h;使eax装有值403045h(十六进制)
mov cx,[eax];把位于内存地址eax的word大小的值(403045)移入cx寄存器。
在mov cx, [eax]中,处理器会先查看eax装有什么值(=内存地址),然后在那个内存地址中有什么值,并把这个word(16位,因为目标-cx-是个16位寄存器)移入cx。
ADD, SUB, MUL, DIV
许多伪代码做计算工作。你可以猜出它们中的大多数的名字:add(加),sub(减),mul(乘),div(除)等。
Add伪代码有如下语法:
Add 目标,源
执行的运算是 目标=目标+源。下面的格式是允许的。
目标 | 源 | 例子 |
Register | Register | add ecx, edx |
Register | Memory | add ecx, dword ptr [104h] / add ecx, [edx] |
Register | Immediate value | add eax, 102 |
Memory | Immediate value | add dword ptr [401231h], 80 |
Memory | Register | add dword ptr [401231h], edx |
这条指令非常简单。它只是把源值加到目标值中并把结果保存在目标中。其他的数学指令有:
sub 目标,源(目标=目标-源)
mul 目标,源(目标=目标×源)
div 源(eax=eax/源,edx=余数)
减法和加法一样做,乘法是目标=目标×源。除法有一点不同,因为寄存器是整数值(注意,绕回数不是浮点数)除法的结果被分为商和余数。例如:
28/6->商=4,余数=4
30/9->商=3,余数=3
97/10->商=9,余数=7
18/6->商=3,余数=0
现在,取决于源的大小,商(一部分)被存在eax中,余数(一部分)在edx:
源大小 | 除法 | 商存于… | 余数存于… |
BYTE (8-bits) | ax / source | AL | AH |
WORD (16-bits) | dx:ax* / source | AX | DX |
DWORD (32-bits) | edx:eax* / source | EAX | EDX |
*:例如,如果dx=2030h,而ax=0040h,dx:ax=20300040h。dx:ax是一个双字值。其中高字代表dx,低字代表ax,Edx:eax是个四字值(64位)其高字是edx低字是eax。
Div伪代码的源可以是
- an 8-bit register (al, ah, cl,...)
- a 16-bit register (ax, dx, ...)
- a 32-bit register (eax, edx, ecx...)
- an 8-bit memory value (byte ptr [xxxx])
- a 16-bit memory value (word ptr [xxxx])
- a 32-bit memory value (dword ptr [xxxx])
源不可以是直接数值因为处理器不能决定源参数的大小。
位操作
这些指令都由源和目标,除了“NOT”指令。目标中的每位与源中的每位作比较,并看是那个指令,决定是0还是1放入目标位中。
指令 | AND | OR | XOR | NOT | ||||||||||
源位 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 |
目标位 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | X | X |
输出位 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 0 |
如果源和目标均为1,AND把输出位设为1。
如果源和目标中有一个为1,OR把输出位设为1。
如果源和目标位不一样,XOR把输出位设为1。
NOT反转源位
一个例子:
mov ax, 3406
mov dx, 13EAh
xor ax,dx
ax=3406(十六进制)是二进制的0000110101001110
dx=13EA(十六进制)是二进制的0001001111101010
对这些位进行xor操作:
源 | 0001001111101010 (dx) |
目标 | 0000110101001110 (ax) |
输出 | 0001111010100100 (new ax) |
新dx是0001111010100100 (十进制的7845, 十六进制的1EA4)
另一个例子:
mov ecx, FFFF0000h
not ecx
FFFF0000在二进制中是11111111111111110000000000000000(16个1,16个0)如果反转每位会得到
00000000000000001111111111111111(16个0,16个1)在十六进制中是0000FFFF。因而执行NOT操作后,ecx是0000FFFFh。
步增/减
有两个很简单的指令,DEC和INC。这些指令使内存地址和寄存器步增或步减,就是这样:
inc reg -> reg = reg + 1
dec reg -> reg = reg - 1
inc dword ptr [103405] -> 位于103405的值步增
dec dword ptr [103405] -> 位于103405的值步减
NOP
这条指令什么都不干。它仅仅占用空间和时间。它用作填充或给代码打补丁的目的。
位移(Bit Rotation 和 shifiting)
注意:下面的大部分例子使用8位数,但这只是为了使目的清楚。
Shifting函数
SHL 目标,计数(count)
SHR 目标,计数(count)
SHL和SHR在寄存器,内存地址中像左或向右移动一定数目(count)的位。
例如:
;这儿al=01011011(二进制)
shr al, 3
它的意思是:把al寄存器中的所有位向右移三个位置。因而al会变成为00001011。左边的字节用0填充,而右边的字节被移出。最后一个被移出的位保存在carry-flag中。Carry-flag是处理器标志寄存器的一位,它不是像eax或ecx一样的,你可以访问的寄存器(虽然有伪代码干这活),但它的值决定于该指令的结构。它(carry-flag)会在后面解释,你要记住的唯一一件事是carry是标志寄存器的一位且它可以被打开或者关闭。这个位等于最后一个移出的位。
Shl和shr一样,只不过是向左移。
;这儿bl=11100101(二进制)
shl bl, 2
执行了指令后bl是10010100(二进制)。最后的两个位是由0填充的,carry-flag是1,因为最后移出的位是1。
还有两个伪代码:
SAL 目标, 计数(算术左移)
SAR 目标, 计数(算术右移)
SAL和SHL一样,但SAR不完全和SHR一样。SAR不是用0来填充移出的位而是复制MSB(最高位)例如:
al = 10100110
sar al, 3
al = 11110100
sar al, 2
al = 11101001
bl = 00100110
sar bl, 3
bl = 00000100
Rotation(循环移动) 函数
Rol 目标,计数;循环左移
Ror 目标,计数;循环右移
Rcl 目标,计数;通过carry循环左移
Rcr 目标,计数;通过carry循环右移
循环移动(Rotation)看上去就像移(Shifting),只是移出的位又到了另一边。
例如:Ror(循环右移)
|
| Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
|
原来 |
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
|
循环移动,计数=3 |
|
|
|
| 1 | 0 | 0 | 1 | 1 | 0 1 1 (移出) |
结果 |
| 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 |
|
如你在上图所见,位循环了。注意,每个被推出的位又移到了另一边。和Shifting一样,carry位装有最后被移出的位。Rcl和Rcr实际上和Rol,Rcr一样。它们的名字暗示了它们用carry位来表明最后移出的位,但和Rol和Ror干同样的事情。它们没有什么不同。
交换
XCHG指令也非常简单。它同在两个寄存器和内存地址之间交换:
eax = 237h
ecx = 978h
xchg eax, ecx
eax = 978h
ecx = 237h