windows下32位汇编语言学习笔记 第十章 内存管理部分 2

这两天通过写汇编程序,越来越发现汇编很有意思。自己规划每一个寄存器的使用,设计每一个跳转和分支,这不同于使用其他高级语言,所有资源对于编程者都是透明的,让我有一种尽在掌握的感觉,而且每写一个程序都很有成就感,这是我用别的语言写程序所没有的感觉。

不管学习什么东西,实践是最重要的,计算机程序设计这种实践性很强的科目更是如此。有的东西看似简单,实际动起手来可就不那么容易了,所以必须要告诫大家,学习计算机程序设计,必须要勤动手,不能懒惰。如果你能够把 windows程序设计,windows核心编程,windows32位汇编语言,这3本书的所有例子自己用汇编写一遍,我可以很负责的告诉你,你已经是高手了。

言归正传,笔记继续


汇编的跳转,分支,循环指令
在继续程序之前,我觉得有必要把汇编的跳转,分支,循环指令总结一下,有一点必须要清楚,我们现在的目的是学习汇编,为将来的更深入的学习逆向打下良好的基础。这两天在写程序的过程中,我发现我背离了我的初衷,看看以前我的代码例子,完全是用C程序的思路换成汇编语法,包括罗云彬这本书里的例子程序也是如此。大量的条件判断使用masm伪指令,比如.if,虽然使用这种伪指令的汇编程序更利于阅读,结构更加清晰,但是,我感觉根本没有学到汇编的精髓,或者说没有立即汇编的真谛。

标号:
标号的定义是,代码中的某个具体位置。
在我们的源代码中,标号就好比书签,让我们设计分支,循环语句时引导程序的运行流程。在编译器中,标号的意义在于标志处跳转指令和目的地址的范围,用以计算这段范围内的字节数,用于生成机器码。
为什么我这么理解,用jmp指令举个例子,先看看下面的代码,这是一个典型的Dialog窗口回调函数。
_DlgProc proc hwndDlg,uMsg,wParam,lParam
  mov eax,uMsg
  cmp eax,WM_COMMAND
  jne _init
  invoke _DlgCmd,hwndDlg,wParam,lParam
  jmp _ret
_init:                ;标记处理init消息              
  cmp eax,WM_INITDIALOG
  jne _close
  invoke LoadIcon,hInstance,IDI_VMALLOC
  invoke SendMessage,hwndDlg,WM_SETICON,ICON_BIG,eax
  jmp _initret
_close:              ;标记处理close消息
  cmp eax,WM_CLOSE
  jne _ret
  invoke EndDialog,hwndDlg,0
  jmp _ret
_initret:              ;对于WM_INITDIALOG消息,处理完成后必须返回1
  mov eax,TRUE
_ret:                ;标记返回              
  mov eax,FALSE
  ret
_DlgProc endp
这是一个正真的(指不用伪指令)汇编语言程序,里面用到得其他转移以后再说,现在先看jmp指令,刚才我说了,在我们的源代码里,标号就好比书签的作用,通过标号,我们指定程序的运行流程。
再看看这段程序反汇编以后的内容,先只关注里面的jmp指令.

00401487  /.  55            PUSH    EBP
00401488  |.  8BEC          MOV     EBP, ESP
0040148A  |.  8B45 0C       MOV     EAX, DWORD PTR SS:[EBP+C]
0040148D  |.  3D 11010000   CMP     EAX, 111                         ;  Switch (cases 10..111)
00401492  |.  75 10         JNZ     SHORT MyVMAllo.004014A4
00401494  |.  FF75 14       PUSH    DWORD PTR SS:[EBP+14]            ; /Arg3; Case 111 (WM_COMMAND) of switch 0040148D
00401497  |.  FF75 10       PUSH    DWORD PTR SS:[EBP+10]            ; |Arg2
0040149A  |.  FF75 08       PUSH    DWORD PTR SS:[EBP+8]             ; |Arg1
0040149D  |.  E8 47FEFFFF   CALL    MyVMAllo.004012E9                ; \MyVMAllo.004012E9
004014A2  |.  EB 3C         JMP     SHORT MyVMAllo.004014E0
004014A4  |>  3D 10010000   CMP     EAX, 110
004014A9  |.  75 1F         JNZ     SHORT MyVMAllo.004014CA
004014AB  |.  6A 65         PUSH    65                               ; /RsrcName = 101.; Case 110 (WM_INITDIALOG) of switch 0040148D
004014AD  |.  FF35 F4304000 PUSH    DWORD PTR DS:[4030F4]            ; |hInst = NULL
004014B3  |.  E8 9A000000   CALL    <JMP.&user32.LoadIconA>          ; \LoadIconA
004014B8  |.  50            PUSH    EAX                              ; /lParam
004014B9  |.  6A 01         PUSH    1                                ; |wParam = 1
004014BB  |.  68 80000000   PUSH    80                               ; |Message = WM_SETICON
004014C0  |.  FF75 08       PUSH    DWORD PTR SS:[EBP+8]             ; |hWnd
004014C3  |.  E8 90000000   CALL    <JMP.&user32.SendMessageA>       ; \SendMessageA
004014C8  |.  EB 11         JMP     SHORT MyVMAllo.004014DB
004014CA  |>  83F8 10       CMP     EAX, 10
004014CD  |.  75 11         JNZ     SHORT MyVMAllo.004014E0
004014CF  |.  6A 00         PUSH    0                                ; /Result = 0; Case 10 (WM_CLOSE) of switch 0040148D
004014D1  |.  FF75 08       PUSH    DWORD PTR SS:[EBP+8]             ; |hWnd
004014D4  |.  E8 67000000   CALL    <JMP.&user32.EndDialog>          ; \EndDialog
004014D9  |.  EB 05         JMP     SHORT MyVMAllo.004014E0
004014DB  |>  B8 01000000   MOV     EAX, 1
004014E0  |>  B8 00000000   MOV     EAX, 0                           ;  Default case of switch 0040148D
004014E5  |.  C9            LEAVE
004014E6  \.  C2 1000       RETN    10

先看第一条jmp指令004014A2  |.  EB 3C         JMP     SHORT MyVMAllo.004014E0,也就是源代码中的jmp _ret。

可以看到,真正编译后,可执行程序里根本没有我们定义的标号,而是直接替换成了一个地址004014E0,把我们代码里的_ret换成里一个地址,让我们看看原理。

在编译程序的时候,编译器负责把汇编源代码翻译成机器码(操作码),操作码都是16进制的数据类型,比如jmp指令的硬件码有2个,E9(near跳转) 和 EB(short跳转)看看第一条jmp指令,硬件码是EB 3C,EB就代表jmp指令,3C是什么?3C就是指令地址到目标地址的一个偏移量,也就是中间这段区域的字节大小。这段距离字节的大小可以这样计算。
偏移量 = 目标地址-跳转指令地址-跳转指令字节数 = 004014E0 - 004014A2 - 2 (EB 3C2个字节) = 3C 
就是通过这样的计算,编译器把jmp _ret代码翻译成了EB 3C 操作码。把我们源代码里的标号语句替换成了实际的目的地地址,总不能让程序员自己去计算跳到那里需要多少字节把。

注:所有的跳转指令都有near跳转和short跳转2种,short跳转(也叫近跳转)指跳转距离在127(0x7F)字节以内,0x7F是1字节的16进制所能表达的最大的正数,再大就是负数了0x80,就成了-128了。
near跳转(也叫长跳转)范围是0x7FFFFFFF之内,就是4字节16进制所能表达的最大正数。
所以对于进跳转,上面计算偏移量的的指令本身长度就是EB+1字节的跳转范围,共2字节,对于元跳转就是E9+4字节的跳转最大范围,共5字节。

汇编的分支,循环,在代码中都是通过标号来确定指令的转移的具体位置,所以必须先要理解标号的作用。

汇编的条件分支
汇编的分支简单的理解就是高级语言中的if else,与高级语言不通的是,汇编的条件分支将高级语言中的if else细化了。看看为什么说是细化了。
比如C语言的if例子:
if(100 < 200) 
   ...
else
   ... 
这个if实际上计算机要做很多工作,分解来看。
1.首先要比较100 < 200 是否成立。
2.如何比较?是用100-200判断得出是否是负数,还是用200-100判断是否是正数?
3.通过上面的2种比较方法的不同答案,确定是继续执行还是跳转到else后面执行。
实际上这个if里的最关键的地方第二步中用什么方式判断100<200,以及转移方法,在高级语言中我们根本不去考虑,也从没考虑过。


标志寄存器
继续学习分支前,先来了解一下汇编中的几个标志寄存器flags register(EFLAGS),下面看看这个寄存器中的“位”于“标志”的关系。
第0 位 CF(Carry Flag) 进位标志位 | 第2 位 PF(Parity Flag)奇偶标志位 | 第6 位 ZF(Zero Flag) 零标志位 | 第7位 SF(Sign Flag) 符号标志位 
第10位 DF(Direction Flag) 方向标志位 | 第11位 OF(OverFlow Flow) 溢出标志位

根本不用背,理解了为什么需要这些标志位,你自然就会记住这些标志位。

其中的CF OF SF ZF 四个标志是与条件分支指令息息相关的,这些条件指令通过对条件运算后所产生的标志位来确定如何跳转。

还是用上面的if(100 < 200)来理解标志寄存器,首先需要计算100<200这个表达式,如果用脑袋想,估计会像下面这样:
1.用100-200,等到一个值-100
2.判断-100是是等于0还是不等于0。(计算机里0代表假,其他数代表真)
3.如果等于0,哦,执行某某地方,如果不等于0,哦,执行某某地方。
实际上成了一个运算,2个判断。

看看计算机是如何处理的,先用汇编来重写这个判断
cmp 100,200
jge else 大于等于跳转
...
else:
...
1.首先也是用100-200。100-200=-100 那么标志寄存器的SF就被置1因为是负数
2.计算机不去理会结果是多少,而是看寄存器中的标志位。如果SF是1,则说明第一个数比第二个数小,就直接跳转。
既不用保存计算结果,也不用把结果再和0比较。计算后通过标志位就知道该如何跳转,这就是汇编的条件跳转指令的执行方式。

条件转移指令分为有符号的和无符号的。
有符号的条件转移指令通过标志寄存器的SF标志来判断是否跳转,而无符号的条件转移指令通过CF标志来判断是否跳转,还有一些条件转移指令通过ZF标志判断跳转。
所有的跳转前都有会有一条指令来改变这些标志位,通常使用cmp 操作数1,操作数2,通过操作数1-操作数2,来改变标志位。条件转移指令紧跟在cmp指令后面进行跳转。


条件转移指令
所有条件转移指令都以J开头后面跟然后是条件或者标志的英文缩写。
Equal(等于) Above,Greater(大于) Below,Less(小于) Not(非),C,O,S,Z(CF,OF,SF,ZF四个标志)
如果有not则n在条件缩写的前面,下面对照高级语言的比较来看汇编的条件指令。以下面的if做模板。
if(a>b)        cmp a,b           
...            jl/jb _else
...            ...
else           _else
...            ...

if(a>=b)  汇编:  jnl/jnb _else
if(a<b)    jg/ja    _else
if(a<=b)    jng/jna _else
if(a==b)    jne/jnz _else
if(a!-b)    je/jz   _else

所有的条件指令全是和高级语言中的判断符号相反,判断> 指令用jl 不小于,判断<,指令用jg不大于,这是因为当cmp指令执行后,当前的标志寄存器的状态就是cmp指令 操作数1 - 操作数2
所产生,必须在其他指令该表标志位前进行条件转移。
Less(小于),Greater(大于)是对有符号数使用的,Above(大于),Bolow(小于)是对无符号数使用的。
cmp eax,100
ja  _big    那么只要EAX的16进制值大于100,就会跳转。00000065-FFFFFFFF ,不判断符号位,大于就跳转。
jg  _big    那么只有EAX里的值大于100,而且不为负数的时候才跳转。00000065-7FFFFFFF ,正数范围内,判断大于。

其实很简单,当你写汇编代码cmp x,y 的时候,下一句的条件转移指令必须是条件不成立时的转移地址。所以反着来写就Ok。


汇编的循环指令
理解了上面的标号,跳转指令,和条件转移指令,就能够写出任何的高级语言中的循环。无非就是这些指令的合理组合。
while()循环
_while:  cmp a,b
    j?? _endwhile ;不成立了就跳到结束
    ...
    jmp _while  ;跳到_while处继续比较
_endwhile:

汇编还有一种简单的循环方式,就是loop,loop指令使用ecx作为计数器,每次执行到loop,ecx将自动-1,知道ecx为0时退出循环。比如:

mov ecx,100  ;循环100次
_for:
...    ;循环体
loop _for  ;循环100次,直到ecx被减到0,停止循环

loop还有loope,loopne,两个指令,用来判断当循环体内某一个条件成立则退出循环。

mov ecx,100  ;循环100次
_for:
...    ;循环体
cmo ecx,3  
loopne _for  ;如果ecx不等于3才继续循环,也就是只循环97次。


汇编的条件指令和高级语言中的条件指令相比,需要关注更多的细节,由于标号的使用,对于程序结构的设计就需要更加小心和细致,否则不仅容易出错还会造成难以维护的后果。唯一的熟练掌握的方法就是,多写,多练,多看别人的程序(最简单的就是反汇编自己用C或者C++写的循环,判断)看看编译器是如何组织的。


最后,贴上我写的一个虚拟内存应用的一个小例子,这个小程序是我结合windows核心编程中,第15章的例子设计的,见图:



Alloc Num用来输入需要保留多少个页面文件。保留后,使用use提交在Index后面Edit文本框输入的值(0<=值<Alloc Num)的这块内存页。Clear用来释放指定值的内存页,Clear All释放所有提交的内存。下面的Memory View查看分配内存的页面信息。每次提交,清除后都会刷新显示。

上传的附件 15-VMAlloc.rar