内存管理
Global标准内存管理
GlobalAlloc函数使用GMEM_MOVEABLE参数返回的是个内存句柄,内容是一个地址,这个地址才是可以使用的内存块。比如:
PVOID lpMem = GlobalAlloc(GHND, 1000);
现在lpMem指向的内容是一个地址,必须通过GlobalLock(lpMem)函数返回这个指针才能使用。
实际上GMEM_MOVEABLE参数和GMEM_FIXED区别就是前者必须要经过GlobalLock才能使用,后者则通过返回的指针直接使用。
关于什么时候可移动,我没法测,不知道windows在什么情况下去移动这块内存。
GlobalAlloc分配内存是紧凑的,就是说分配的多个内存空间可以放在一个内存页面里,只要能放的下。
Heap堆内存管理
Heap内存管理非配基本上和Global的用法相当,一般的用法都是GetProcessHeap得到进程默认堆,然后HeapAlloc分配。
虚拟内存管理
VirtualAlloc,所分配的空间都是页对齐的,分配1字节的内存空间也会占用4096字节的一个内存页,再次使用VirtualAlloc分配内存会从新的一页开始。
每个虚拟内存页面有三种状态:Free:自由状态,Commit:提交状态,Reserve:保留状态。
八种保护属性:
PAGE_NOACCESS,PAGE_READONLY,PAGE_READWRITE,PAGE_EXECUTE,PAGE_EXECUTE_READ,PAGE_EXECUTE_READWRITE,PAGE_EXECUTE_WRITECOPY,PAGE_WRITECOPY
这八种属性在第一章的笔记里有介绍。
虚拟内存页面还有4种类型
Free:空闲,没有提交也没有使用的。Private:私有,该区域的虚拟地址和系统页文件关联。Image:映像,改区域的虚拟内存地址和exe文件或dll文件关联。
Mapped:映射,改区域的内存和其他硬盘上的文件关联。
这些页面的属性,类型,状态,就是80386保护模式下,windows系统管理虚拟内存的机制。
虚拟内存管理函数中提供了几个可以操作其他进程虚拟地址的函数。
LPVOID VirtualAllocEx(HANDLE hProess,LPVOID lpAddress,SIZE_T dwSize,DWORD flProtect)
这个函数可以在别的进程空间保留或者提交一块内存。比如DLL的远程注入就是使用这个函数在目标进程开辟一块虚拟内存空间,将要注入的dll名通过WriteProcessMemory函数写进目标进程的虚拟内存空间,然后再通过CreateRemoteThread函数运行LoadLibraryA加载写入的dll文件,LoadLibraryA函数位于kernel32.dll中,由于每个进程运行都会加载这个dll,所以LoadLibraryA函数在不同进程中的地址是一样的,可以直接再目标进程使用。
Code
// 向目标进程地址空间写入DLL名称
DWORD dwSize, dwWritten;
dwSize = lstrlenA( lpszDll ) + 1;
LPVOID lpBuf = VirtualAllocEx( hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE );
if ( NULL == lpBuf )
{
CloseHandle( hProcess );
// 失败处理
}
if ( WriteProcessMemory( hProcess, lpBuf, (LPVOID)lpszDll, dwSize, &dwWritten ) )
{
// 要写入字节数与实际写入字节数不相等,仍属失败
if ( dwWritten != dwSize )
{
VirtualFreeEx( hProcess, lpBuf, dwSize, MEM_DECOMMIT );
CloseHandle( hProcess );
// 失败处理
}
}
else
{
CloseHandle( hProcess );
// 失败处理
}
// 使目标进程调用LoadLibrary,加载DLL
DWORD dwID;
LPVOID pFunc = LoadLibraryA;
HANDLE hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, lpBuf, 0, &dwID );
DWORD VirtualQuery(PVOID pvAddress,PMEMORY_BASIC_INFORMATION lpBuffer,SIZE_T dwLength);
这个函数可以用来查询pvAddress指定的内存空间的状态State,类型Type,和保护属性Protect,地址空间的属性保存在MEMORY_BASIC_INFO结构中。用这个函数,就可以写一个和Cheat Engine里一样的查看内存Region的程序。
下面这个小程序就是使用VirtualQuery函数实现查询当前进程中的内存区域状态。实现的功能比Cheat Engine里的还多了可以知道哪里的内存是进程堆,那里映射,映像了什么文件
Code
;显示内存信息
.386
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include macros.asm
include user32.inc
includelib user32.lib
include Psapi.inc
includelib Psapi.lib
IDD_DIALOG equ 104
IDC_LIST equ 1001
.data?
hInstance dd ?
hWinMain dd ?
hProcess dd ?
.const
msg db "%10p %15s %15s %10p %10s",0
memfree db "MEM_FREE ",0
memcommit db "MEM_COMMIT ",0
memreserve db "MEM_RESERVE",0
unknow db ' ',0
memimage db "MEM_IMAGE ",0
memmapped db "MEM_MAPPED ",0
memprivate db "MEM_PRIVATE",0
.code
;内存状态
_FormatState proc wState
mov eax,wState
.if eax == MEM_COMMIT
lea eax,offset memcommit
.elseif eax == MEM_FREE
lea eax,offset memfree
.elseif eax == MEM_RESERVE
lea eax,offset memreserve
.else
lea eax,offset unknow
.endif
ret
_FormatState endp
;内存类型
_FormatType proc wType
mov eax,wType
.if eax == MEM_IMAGE
lea eax,offset memimage
.elseif eax == MEM_MAPPED
lea eax,offset memmapped
.elseif eax == MEM_PRIVATE
lea eax,offset memprivate
.else
lea eax,offset unknow
.endif
ret
_FormatType endp
;保护属性
_FormatProtect proc wProtect,szBuffer
mov eax,wProtect
.if eax == PAGE_READONLY
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT('-R--')
.elseif eax == PAGE_READWRITE
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("-RW-")
.elseif eax == PAGE_WRITECOPY
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("-RWC")
.elseif eax == PAGE_EXECUTE
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("E---")
.elseif eax == PAGE_EXECUTE_READ
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("ER--")
.elseif eax == PAGE_EXECUTE_READWRITE
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("ERW-")
.elseif eax == PAGE_EXECUTE_WRITECOPY
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("ERWC")
.else
invoke wsprintf,szBuffer,offset CTXT('%s'),CTXT("----")
.endif
ret
_FormatProtect endp
;显示内存信息
_ShowMemoryState proc uses ecx hwndLB
local @msg[1024]:byte
local @path[MAX_PATH]:byte
local @mbi:MEMORY_BASIC_INFORMATION
local @Ret:dword
local @szState:dword
local @szType:dword
local @szProtect[5]:byte
local @pHeapAddress:dword
;ebx作为下一个查询的内存地址
xor ebx,ebx
_loopbegin:
invoke VirtualQuery,ebx,addr @mbi,sizeof @mbi
mov @Ret,eax
mov eax,@mbi.BaseAddress
invoke _FormatState,@mbi.State
mov @szState,eax
invoke _FormatType,@mbi.lType
mov @szType,eax
invoke _FormatProtect,@mbi.Protect,addr @szProtect
invoke wsprintf,addr @msg,offset msg,@mbi.BaseAddress,@szState,@szType,@mbi.RegionSize,addr @szProtect
;标志内存映射,映像文件
.if @mbi.State != MEM_PRIVATE
invoke GetCurrentProcess
mov hProcess,eax
invoke GetMappedFileName,hProcess,ebx,addr @path,MAX_PATH
.if eax
invoke lstrcat,addr @msg,CTXT(' ')
invoke lstrcat,addr @msg,addr @path
.endif
.endif
;标志堆地址
invoke GetProcessHeap
mov @pHeapAddress,eax
.if ebx==eax
invoke lstrcat,addr @msg,CTXT(' ')
invoke lstrcat,addr @msg,CTXT('Process Heap Address')
.endif
invoke SendMessage,hwndLB,LB_ADDSTRING,0,addr @msg
add ebx,@mbi.RegionSize
cmp @Ret,sizeof @mbi
je _loopbegin
ret
_ShowMemoryState endp
_DlgProc proc hWnd,wMsg,wParam,lParam
mov eax,wMsg
.if eax == WM_CLOSE
invoke EndDialog,hWnd,0
.elseif eax == WM_INITDIALOG
invoke GetDlgItem,hWnd,IDC_LIST
invoke _ShowMemoryState,eax
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_DlgProc endp
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_DIALOG,NULL,offset _DlgProc,WM_INITDIALOG
invoke ExitProcess,NULL
end start
代码:
//资源文件 使用ResEdit编辑
// Generated by ResEdit 1.4.4.19
// Copyright (C) 2006-2008
// http://www.resedit.net
#include "res.h"
#include <windows.h>
#include <commctrl.h>
#include <richedit.h>
//
// Dialog resources
//
IDD_DIALOG DIALOGEX 0, 0, 535, 252
STYLE DS_MODALFRAME | DS_SETFONT | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP | WS_SYSMENU
EXSTYLE WS_EX_WINDOWEDGE
CAPTION "Dialog"
FONT 10, "Courier New", 400, 0, 0
BEGIN
LISTBOX IDC_LIST, 0, 0, 535, 250, WS_TABSTOP | WS_VSCROLL | LBS_NOINTEGRALHEIGHT | LBS_SORT | LBS_NOTIFY
END
通过循环调用VirtualQuery,通过返回的mbi结构获得所有进程地址空间的各种信息,从0地址开始查找,每次查找后把地址加上mbi结构的RegionSize(区域尺寸)再查找下一个区域的信息。
还可以通过DWORD GetMappedFileName(HANDLE hProcess,LPVOID,lpv,LPTSTR lpFilename,DWORD nSize)检查类型为MEM_IMAGE,MEM_MAPPED的内存区域加载文件的路径。
还可以通过HANDLE GetProcessHeap(VOID)获得进程堆地址。
程序里用到了Masm宏CTEXT(),用这个宏就不必每次使用个字符串都去.data或.const里定义,这个宏实际上就是实现再.data 或者.const定义一个字符串。
汇编的串指令
汇编中有一些专门用来处理连续内存单元的指令,叫做串操作指令。串指令通过EDI或ESI来指定,可以对内存单元按字节,字或者双子进行处理,并更具操作对象的字节数根据DF(方向标志)对DEI,ESI变址寄存器自动增减1,2,或4字节.
先看看书里的例子: 将szSource中的内存移动到szDest中.
mov esi,offset szSource
mov edi,offset szDest
mov ecx,dwSize
cld
rep movesb
cld指令是清方向指令,使DF标志为0,使每次移动一个字节,esi和edi都加1。
movesb 是串移动指令,后面的b代表byte每次移动一个字节类型,还可以是movesw(移动一个字),movesd(移动一个双字),并根据方向位和字节大小对dsi和edi进行增减.
rep是重复字符串操作指令,后面跟一个串操作指令来重复串指令,重复的次数由ecx来决定,所以,上面例子中把dwSize放到ecx寄存器中,用来指定内存单元大小。
stosb/stosw/stosd指令的作用是把al(1字节),ax(2字节),eax(4字节)的值,填充EDI指向的内存区域。同样根据DF来对EDI进行增减。
lodsb/lodsw/lodsd指令是把ESI指向的内存区域取一个字节,2字或者4字节,放入al,ax,eax中,根据DF标志位对ESI进行增减。
汇编的跳转,分支,循环指令
标号:代码中的某个具体位置。
在我们的源代码中,标号就好比书签,让我们设计分支,循环语句时引导程序的运行流程。在编译器中,标号的意义在于标志处跳转指令和目的地址的范围,用以计算这段范围内的字节数,用于生成机器码。
为什么我这么理解,用jmp指令举个例子,先看看下面的代码,这是一个典型的Dialog窗口回调函数。
Code
_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指令.
Code
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章的例子设计的
...
来源:http://bbs.pediy.com/showthread.php?t=89608