六 Prefixes Segment override and LOCK(二)
有少数语句不是很好翻译,后面有相应的原文以供对照.
重要的一点是DS,SS,CS段都只是用户态段的别名.(Important thing to remember is that DS SS CS segment are alias segments in Win32 user mode app. )这意味着我们可以通过任一段寄存器寻址数据.
如果你正试着做"String到Dwords的转换",你可以使用栈操作,当栈顶指针直接指向.data节,使用push可以直接将常量字符串放置在数据节.
同样,使用.code节的数据可以这样子:
.code
msg db 'Some text',0
start:
invoke MessageBox,0,offset sometext,......
另外,下面的代码完成的功能是一致的:
.data
somedw dd 12343
......
.code
......
mov eax,somedw
mov eax,dword ptr offset somedw
mov eax,dword ptr DS:offset somedw
mov eax,dword ptr CS:offset somedw
mov eax,dword ptr SS:offset somedw
......
我们还须记住的是某些节上的数据可能受到保护,(可以在链接的时候使用/section:[sectionname] option选项改变).然而如果你可以用"ptr somedata"的方式寻址数据,那么你也一定可以任意使用DS,CS,SS作为段选择子.我们将在系统编程方面详细讨论这个.
那么剩下的寄存器呢?
我们先看ES,我们在串操作指令经常要关心它.
在DOS时代我们经常要比较ES是否等于DS,来确认我们的源和目的是不是在同一个段.
在Win32用户态编程中我们不需要这样,但这并不是说ES不再被串操作指令使用.
我们开始试验之前先做点笔记:ES在包含源和目的两个内存操作数的串操作指令中仍然被使用.
例如:movsb = move dword ptr DS:ESI to ES:EDI
那么如果对于上例包含两个内存操作数的指令运用Segment override,那么是哪个段选择子被改变呢?
输入:
A5 65 A5
A5 movs [dword es:edi], [dword ds:esi]
65:A5 movs [dword es:edi], [dword gs:esi]
我们看到Segment override改变了源.改变目的选择子是不被允许的!如果你想改变目的,你可以改变ES的值,而不是改变作为选择子的寄存器.所有的包含两个内存操作数的串操作指令都是如此.
顺便说一下,如果你开始试着去查看OpCode,那么你肯定会看到所有的串操作指令都是单字节的,假设你不使用66 prefix去改变操作数大小,那么在速度允许的情况下,可以写出非常简洁的代码.
我们先前说到,ES仍然在被使用,但是我们不需要特意的用DS去初始化它,当系统加载程序的时候就已经为我们做好这个了.然而你可能会轻易的破坏它的值然后糟糕的后果立马而至.
我们看看选择子是如何影响串操作指令的.栈从高址往低址生长,这意味这我们可以使用比当前栈顶指针小的地址,而不会破坏栈内的数据(返回地址,参数等).例如,在OllyDbg里当前栈顶的内容为:
0012FFC4 77F1B9EA RETURN to KERNEL32.77F1B9EA
0012FFC8 0012E2D4
0012FFCC 77F92CD4 ntdll.77F92CD4
0012FFD0 7FFDF000
所以我们可以使用0012FFC0以下的地址空间,需要注意的一点是默认的栈大小是1MB,当然可以在链接的时候使用相应的选项去改变这个值;并且不要使用开头的1kb,那是用来捕捉错误的.
你可以使用一些push和pop指令观察栈的变化情况.
现在我们给局部变量分配一些空间来检测一下movs对于不同的选择子是怎么样工作的.我们要做的是:
1.用短的字符串填充栈空间
2.以字节为单位将字符串拷贝到接近栈的区域,并且每一步我们指定不同的段寄存器
- to get impression that selectors in DS,SS,CS pointing to alias segment
- to check if ES is still matter by spoiling it in last step
输入:
lea edi,[esp-4]
现在edi指向局部变量所在的空间,所以我们不用担心破坏栈中的返回值和参数等等.我们将使用小于等于esp-4的地址空间.
输入助记符
std
我们设置方向位为1,串操作指令将使用递减的地址.顺便说一下,当你在窗口回调函数中置方向位,请务必记得在返回前清方向位,因为回调函数是通过置DF=0为系统返回值的.(When you change DF to 1 in your window callback procedure - remember to set it to 0 before your proc returns - your proc returns to system code and the code assumes that DF=0)
输入另一个助记符
mov al,5
stosb
dec al
输入OpCode
75 FB
这些指令类似于下面的汇编源码:
lea edi,[esp-4]
std
mov al,5
@@:
stosb
dec al
jne @B
这里面有一个问题,短转指令的二进制格式是
0111tttn:imm8
前四位0111确定了只是一个段跳转指令,在调试器里它表现为第一个十六进制位是7,接着的四位是用来确定条件的”tttn”位域,这和许多指令中用来测试标志位的格式是一样的.第二个字节是在跳转指令被解码后将被加到EIP的有符号数.
我们试着解码OpCode 75 FB:
7 短条件跳转的标志
5 tttn 0101,0100 代表‘e’,zf = 0;0101 代表‘ne’,zf = 1;可以看到改变最后一位就相当于把条件改变位‘非’,这就是为什么我们叫它“tttn“.
FB 即-5,我们需要EIP会跳5个字节
1字节 AA STOS [BYTE ES:EDI]
2字节 FEC8 DEC AL
2字节 75 FB JNZ SHORT
------
5字节
好,我们回到"register override".
切换到数据窗口,在OllyDbg的Command文本框里输入命令 d esp-4 ,并往上面滚动一行,这样好看到当前栈顶的内容,因为我们将用字符串来填充esp-4往下(地址变小)的空间.我们按F8单步执行,可以清楚的看到字节01 02 03 04 05 是如何存放的.
我们首先来验证一下DS,CS,SS是否是段选择子的别名.(First we check if DS CS SS are alias semgent selectors)
输入
lea esi,[edi+5]
这条指令执行后esi将指向之前我们构造的字符串(01 02 03 04 05).并且edi已经指向该字符串的末尾,这样我们就可以开始拷贝了.
movsb的OpCode是 A4
A4 = COPY FROM [BYTE DS:ESI] TO [BYTE ES:EDI]
在A4前使用segment override prefix我们就能改变源选择子,这样的prefix有CS-2E,SS-36,ES-26.
我们先看看默认的情况:
输入
A4
然后使用F8单步执行,并观察数据窗口内容的变化.
现在我们看看CS的情形:
输入
2E A4
然后使用F8单步执行,并观察数据窗口内容的变化.
同样测试一下36和26.
如果操作正确,你将可以看到字符串的头四个字节被成功拷贝,这表明(That shows that all SS DS CS ES
pointed to the same alias segment through different selectors.).
你可以试一下别的segment override prefix,你得到的肯定是一个错误提示.
我们用刚才字符串剩下的一个字节来测试一下ES.
输入
66 6A 00
即push word 0
输入
pop es
A4
同样的,按F8键,当到达lodsb指令行时,OD的状态栏会提示异常.
这表明ES仍然在双内存操作数串操作指令中被使用.我们可以输入
push ds
pop es
来解决刚才的问题.
执行这两条指令,然后输入A4,然后一切正常.
值得提到的另一个段寄存器是FS,FS被用作异常处理.然而现在你记住这句话就好了,每当你使用FS操作数据的时候,将多消耗你一个字节以及一个时钟.
当然使用这个寄存器要看你的意图是什么.
下一个Prefix是LOCK.
如果在Pentium 和 Pentium MMX下使用如F00FC7C8这样的指令,可以冻结处理器,在这里我仅仅引用其中的一些描述.如果你想了解这个可以去看Dr.Dobbs的文章.
对于LOCK prefix我没有什么要说的,在Intel的手册里有很好的解释.在结束这部分之前我想提一些关于prefixes的特殊的用途,但是在P III下处理器会忽略掉这些,Intel提出在新的模型中那些特殊的用法可能有新的特殊意义,不正当的用法可能导致一些不可预料的结果.关于新一代Prefixes应该提到3E “hint” prefix.
只有在多处理器系统才需要LOCK.当对内存操作数执行如下操作过程:
1.读取内存
2.在算术逻辑单元操作读取到的内存变量
3.写回内存
这时才需要.
可以看到在第1和第3阶段有一段间隔,当目前的处理器正在处理读取到的内存数据的时候,其他处理器也可以读取同一个内存数据,最后可能导致其中的一个被覆盖.而LOCK信号可以阻止任何其他处理器访问共享内存,直到指令将数据写回内存.大多在同步系统中被使用.
