三 开始和结束
三 开始和结束
EB:imm8 --- near jmp
Where commands start and end .why 2 + 2 = 3
虽然这部分基于一些非常简单的事实,但并不是每一个程序员都能看到这些事实背后的本质.所以我们最好不要太过于轻视它.
用OllyDbg 加载上次编写好的“Nop”程序.看着左列的指令地址,起始的指令地址是多少,在我的程序里是:
00401000> 90 nop
再看看右边的寄存器窗口,EIP的值又是什么,在我的程序里是
EIP 00401000
EIP寄存器指向内存中某个字节的地址,这个地址将被认为是某条指令的开始地址.当内存中的字节未被处理器加载并解码,还不知道这条指令到何处结束.
现在我们按F8执行,可以看到
00401001 90 nop
EIP: 00401001
EIP指向了内存中下一个字节并把它当作是指令.这里的下一个字节是指紧接上一条指令的结束地址的那个字节.如果你认为我没有写“EIP指向下一条指令”是因为我糟糕的英语,那你就错了.(尽管你并不是错在我糟糕的英语^_^(原作者是俄罗斯人)).
如果一切正常的话,这些字节将被解码然后被当作下一条指令执行.事实上这些字节可能仅仅是一些垃圾,而处理器则不能正确的对其解码而产生异常.
在程序的当前代码行(高亮显示的那一行)输入OpCode
FFFF
OllyDbg不会把它当作是一条指令,你将在助记符那一栏看到 “???” .同时EIP指向我们刚刚输入的FFFFh.所以如果不管这里的垃圾(对于处理器来说是“非法指令”)的话,处理器将解码并执行它.我们按下F8键,可以看到提示的错误信息(在OllyDbg下方的状态栏),处理器产生了异常.相应的,OllyDbg中断并询问如何继续.
我们将FFFFh 替换为 9090h,然后输入不同的助记符,比如长指令或者使用lea指令,单步执行并仔细观察EIP 和指令长度的变化.你将看到处理器识别出指令的尺寸,且EIP现在指向紧接着刚输入的指令即nop.
处理器是如何做到这个的呢?
事实上处理器是通过解码来正确识别指令的.通过分析指令格式可以知道组成指令的域以及域的大小,不同的位指示了某个大小的域被使用.
例如:短跳转指令EB: imm8 为两个字节,EB 是CODE域,当EIP指向的内容是EB 09,处理器通过解码第一个字节将得知EB 的指令长度是2个字节.
所以问题的一个初步回答是:
1. 开始:处理器认为当前EIP指向的内存单元中的第一个字节就是指令的开始.
2. 结束:处理器通过对OpCode进行解码(大多数情况是根据[ code ]域),从而得知哪里是结束
3. 如果指令不是“控制类型“的,那么下一条指令的开始将紧接当前指令的末尾.
注意:除非被处理器加载,处理器是没有办法确定这些内容是否是真正的合法指令.
现在回顾一下第三点,什么是“控制类型“指令?一般来说它们是ret,call,jmp,jcc,int等.它们之间的共同点是什么呢?不要回答我:”它们都跳转到某个地址“.或者更加糟糕的回答:”它们跳转到另外某条指令开始的地方“.
我们在程序的当前行输入EB 00,并按F8执行.它将跳到哪里?我们看到处理器停在下一条指令开始的地方.我们再输入EB FE,运行,但是并没有跳转到任何地方,就像所有的事情都停止响应一样.就算是普通的指令执行后,也会步向下一条指令,而现在却既不跳转也不步进到下一条指令.
真正的底层程序员应该理解指令的本质,而不单单从指令的字面意义上去理解它.真正的底层程序员不会说cmp指令比较操作数;他会说cmd指令是用第一个操作数减去第二个操作数,由此来设置相应的标志位.同时,我们关心的只是标志位,并不关心键操作后的结果,所以不需要把减操作的结果储存到第一个操作数中.这才是cmp指令的本质.
如果知道了它是如何工作的,你就可以用cmp指令来做任何它擅长的事情,而不仅仅是比较.其实并没有什么cmp指令,有的只是简单的逻辑的,数学和数据转换\写入\读取的OpCode.
让我们回到“控制指令”.它们真正做了什么样的操作呢?答案是:它们改变EIP寄存器.
我们知道EIP指向的内存数据被处理器当作是指令的开始.
通过这些控制指令我们又可以将什么值赋给EIP呢?
答案是:如果你非常了解OpCode和这些指令的算法,那么可以是任何32位的值.所以我们可以强迫处理器把内存中任意某个字节当作指令的开始.
输入OpCode
04 AC
你将看到
00401008 04 AC ADD AL,0AC
我们也知道ACh是lodsb的OpCode,地址00401008是OpCode 04 AC的起始地址,但是被地址00401009指向的指令是什么呢?如果我们使EIP指向00401009,那么处理器将不会把ACh当作是04:imm8的操作数,而是把它看做一个OpCode ACh->lodsb?
现在如果我们需要实现一个算法
IF ZF = 0
Add al,ACh
ELSE
Lodsb
请试着用助记符写下这个算法.
请看下面的汇编代码:
Jne $+1
Add al,ACh
这就是我们所需要的.请不要试着运行它,除非你清楚的知道[esi]指向的地方是什么内容.
如果ZF = 1,EIP将指向指令add al,ACh的第二个字节,因为我们知道第二个字节的值是ACh,而操作码是ACh ->lodsb,我们实现这个算法仅用了4个字节!!!
00401005 75 01 JNZ SHORT
00401007 04 AC ADD AL,0AC
我们一起来看看各个字节都表示什么意思.
75:imm 是75 01 的域格式,75是JNZ的OpCode,imm在这里是01,会被加到EIP里面去,整个7501表示:如果这条指令被执行了,则EIP会指向下一条指令的第二个字节.04 AC的域格式:04:imm,04->OpCode,AC->imm.两条指令都有两个域,格式为:,[ code ] [ imm ].
如果ZF = 0,7501这条指令就会把EIP中下一条指令的起始地址 + 1,使得EIP指向下一条指令的第二个字节,处理器将认为AC所在的地址才是下一条指令的开始,这时AC将被认为是一个新的OpCode.
否则EIP将指向04 AC所在的地址,开始的字节是04h,处理器将认出格式域:04:imm8(add al,imm8),这时AC将被当成操作数,而不是操作码.
现在我们看看谁是最快最聪明的程序员.
1. 实现一个4字节的算法:
IF ZF = 1
Inc eax
ELSE
Mov al,40h
2. 另一个测试(5字节):
IF PF = 1
Set all bits in ax to 0
ELSE
Set all bits in eax to 0
3. 再一个测试(5字节):
IF PF = 1
Set all bits in eax to 1
ELSE
Set all bits in ax to 1
(Try it yourself)
(1 提示: je $ + 1
Mov al,40h
74h,01h,B0h,40h )
(2 提示: jpe $ + 3
Xor eax,eax
74h,01h,66h,31h,C0h)
原文第三帖
