windows下32位彙編語言學習筆記 第一章
第一章 背景知識
80x86處理器的存儲器
4個資料寄存器
EAX,EBX,ECX,EDX
EAX寄存器
所有API函數的傳回值都儲存在EAX裡,注意是傳回值,不是傳回參數,本書3.2.2 節,說是winapi的傳回值,
而經過我測試,自定義函數的傳回值也一樣儲存在eax裡。
EBX寄存器
這個寄存器被windows用來儲存指針使用前後必須push pop,EDI,ESI,ESP也是一樣,彙編定義函數有個uses 指令,如果要用,
就在uses後面寫上寄存器的名字。比如 _mypro proc uses ebx,para1,para2 ,這樣編譯後就自動在前面加上push ebx,傳回前加上 pop ebx。
ECX,EDX,這倆随便用
彙編的這幾個寄存器,感覺就像是全局變量,随便用,挺友善,但要記住幾個關鍵寄存器使用前要儲存,使用後要恢複,而且最後把傳回結果mov 給eax就行。
2個變址指針寄存器
EDI,ESI
這倆寄存器,在字元串操作方面真是恰倒好處,主要配合rep\repz\repnz\repe\repne\cmpsb\movsb\stosb\lodsb\cmpsw等。比如将一個szMsg的字元串複制到記憶體指針lpMem,并且去掉字元串中的大寫A:
pusha
lea esi,szMsg
mov edi,lpMem
@@:
cld
lodsb
cmp al,0
je @F
test al,'A'
jz @B
stosb
jmp @B
@@:
popa
2個指針寄存器
EBP,ESP
EBP
存取堆棧的指針,棧内的參數就是用這個來取的。先push ebp 然後 mov ebp,esp 然後 ebp - x 來取參數,最後也要pop 出來,恢複原先的值。
ESP
棧頂的指針,這個棧是向下擴充的,棧的大小可以設定,預設是大小有的說是64k,有的說是1M,可以用 .STACK[位元組數]來指定棧的大小。
每個線程都有自己的棧,棧就是用來儲存參數,局部變量,和傳回位址。堆棧的大小是有限制的,是以壓多少就要彈出多少。
用于操作棧的指令:
push xxxx 壓棧,pop xxxx 出棧,pushad 把八個通用寄存器依次壓棧,popad 八個寄存器彈棧,也就是常說的保護現場環境。
堆棧平衡:每個線程(程序隻是個核心對象,線程才是執行程式功能的)都有自己的棧,很明顯棧的大小是有限制的,壓了幾個變量進去用完就要清除掉。
另外call子程式的時候,當執行call的時候,就把調用位址壓入棧,子程式中你不清除壓入棧的變量,ret就沒法傳回到調用處了。
以前的dos程式函數參數比較少,可以通過寄存器傳遞,到了win32,API的參數猛增,就4個資料寄存器怎麼能夠用,是以就用棧來傳遞參數。
8個通用寄存器全是32位的DWORD類型,這也是win32API參數全是DWORD類型的原因,操作起來友善。
感覺彙編反而友善了,資料類型不用太操心,不用定義變量儲存函數傳回值,還有4個全局變量(4個資料寄存器)随時可以用,感覺還挺舒服的。
80x86處理器的工作模式:
實模式,保護模式 虛拟86模式,實模式,虛拟86模式就是為了相容老軟體,系統應用而做的一種向下相容的功能,沒必要深入了解,保護模式才是目前win32 下CPU的工作模式。
保護模式下最關鍵的地方就是記憶體尋址空間增加到4G;采用了優先級機制,分四個級别0-3,0級系統級最高,3級使用者級别最低,1 2 是為了相容alpha設定的,用不到。
經常說OD是ring 3級的程式調試工具,這個3級就是指保護模式裡的3級使用者級。
保護模式下使用者級的程式不能夠通路到系統級資源,通過級别的設定,使用者級的程式無法通過提升自己的級别來作業系統級的資源。
windows的記憶體管理
記憶體管理這塊,也可以參照windows核心程式設計 記憶體管理部分
首先每個程序自己的4G尋址空間不是完全可用的
NULL指針區域
0x00000000-0x0000FFFF:65535位元組 這個區域的作用是用來幫助程式員發現記憶體配置設定失敗後未檢查就使用的錯誤。
比如使用malloc配置設定記憶體失敗,傳回NULL,而又未做檢查直接使用,如例子:就會産生記憶體非法通路的錯誤,提示程式員
int *piNum = (int*)malloc(sizeof(int));
int *piNpm = 5;
以前一直不了解NULL的意思,一直以為就是個0,現在來看,這個空指針是有他的道理的,是利用了windows的記憶體管理機制做的一個記憶體使用的檢測手段。
現在看NULL定義為0-65535之間的任何數都可以達到,檢測指針區域的效果。
64K禁入區域
0x7FFF0000-0x7FFFFFFF:64K位元組 用來隔離使用者空間和核心空間,是一個分界線。
實際上程序可用的位址空間最後是到0x7FFE1000,到0x7FFF0000之間的60K記憶體空間就不讓使用了。可以用Chect Engine 的Memory regions 檢視程序的記憶體空間情況。
windows核心空間
0x80000000-0xFFFFFFFF:2G 這個分區用來儲存作業系統代碼,記憶體管理,線程排程,檔案系統支援,網絡支援,和所有裝置驅動代碼都存放在這裡,這個區域被所有程序共享。同樣也是保護的,不可通路。
其中0x80000000-0xC0000000:1G 用來加載系統所需DLL,SYS,可以用Process Explorer 檢視System程序可以看見系統自己加載的子產品,大部分是.sys驅動,dll隻有ntdll.dll
nv4_disp.dll等極少數的dll子產品,确實是所有裝置驅動的代碼都再這裡。
這塊的記憶體不能通路,我想這也就是為啥驅動級的保護殼厲害,就厲害在這裡...
剩下的1G 0xC0000000以後的記憶體,不知道怎麼看,windows好像就沒提供操作這塊記憶體的API.
必須推薦下Process Explorer,這個應該是windows下功能最強的程序管理器了,看線程,程序,程序子產品等等資訊非常友善。還沒用上的一定要試試。
使用者空間
0x00001000-0x7FFFFFFF:2G-128K 可執行檔案和使用者自己的dll都加載到這個空間。系統DLL加載到系統核心空間
其中0x00001000-0x00400000 是Dos相容分區,這個還有用麼?4M的空間...
0x00400000-0x10000000 是程序相關内容存放區域,這就是為啥預設的可執行檔案加載位址是從0x00400000開始
0x10000000-0x80000000 是使用者DLL映射空間,這就是為啥預設的dll檔案加載位址是從0x10000000開始
從上面看出,并不是所有4G的尋址空間都是可用的,實際可供程序使用的隻用2G-128K的空間。
一直有一個說法,windowsXP無法管理2G以上的記憶體,實際上是:一個程序裡無法使用2G以上的記憶體空間,即使你有4G的記憶體,一個程序,也隻能使用其中的2G。
但是别忘了每個程式都可以用2G的記憶體,你實體記憶體越大,可以同時打開的程序就越多。話說回來,一個程式需要2G以上的記憶體運作,誰用?...
說windows無法管理2G以上的記憶體應該是斷章取義的說法。
配置設定粒度和記憶體頁面大小
x86處理器平台的配置設定粒度是64K,32位CPU的記憶體頁面大小是4K,64位是8K,保留記憶體位址空間總是要和配置設定粒度對齊。一個配置設定粒度裡包含16個記憶體頁面。
這是個概念,具體不用自己操心,比如用VirtualAllocEx等函數,給lpAddress參數NULL系統就會自動找一個地方配置設定你要的記憶體空間。如果需要自己管理這個就累了......
一個配置設定粒度是64K,這就是為什麼Null指針區域和64K進入區域都是 64K的原因,剛好就是一個配置設定粒度。
一個記憶體頁是4K,這就是為什麼PE檔案中的section都是0x1000對齊.
硬碟扇區大小是512位元組,這就是為什麼PE檔案預設檔案對齊是0x200.
這些數字絕對不是心血來潮設定出來的,而是綜合了硬體結構和作業系統架構設定的。
記憶體頁面的各種屬性
PAGE_NOACCESS 禁止寫入執行讀取
檢視程序記憶體區域能發現,NOACCESS屬性的記憶體頁面都是FREE狀态的(未送出使用的記憶體區域),隻有記憶體區域最後的0x7FFE1000-0x7FFF0000之間的60K記憶體區域狀态是Reserve。(保留了,不讓使用...)
PAGE_READONLY PAGE_READWRITE PAGE_EXECUTE 根據字面就很好了解
PAGE_WRITECOPY PAGE_EXCUTE_WRITECOPY 這2個頁面屬性是windows節省記憶體應用的一個機制.
難道要2個一樣的可執行程式同時運作時各占一個獨立4G的尋址空間麼?既然是一樣的程式,2個程式的代碼段,資料段都是相同的。為了節省記憶體,windows就讓2個程序共享單個記憶體塊。
但是如果一個程式中的記憶體發生變化,另一個也同時發生變化,那豈不亂套了?開2個IE浏覽網站,但是2個都顯示同樣的内容那還有什麼意義?copy-on-write就是為解決這個問題而設定的。
PAGE_WRITECOPY 資料段
簡單的說,2個一樣的程式運作,如果記憶體中資料不發生變化,那麼這段資料是共享的,如果其中一個程式的記憶體發生變化,比如記事本A寫了一行字,那麼就會把記事本的這個資料段複制出來
一份放到新的記憶體區域讓記事本A單獨使用,這時候記事本A和記事本B程序的資料段就不再共享,而是各自用各自的。但是他們的代碼段還是共享。
PAGE_EXCUTE_WRITECOPY 代碼段
代碼段也是一樣,你用OD修改了A記事本中的代碼段,系統就會自動把A記事本的代碼段複制一份新的,不再和B共享,也就不會影響B記事本中的代碼段。
實際上一個程式的代碼段,資源段等資料也沒多大。是以,這種機制也看不太出來能節省很大的記憶體。
關于記憶體機關
記憶體機關再書裡,彙編裡,都是用16進制機關描述的,10進制看習慣了,突然全16進制我就比較不習慣。我把常用的列出來,看長了就能有個大概的概念了,突然來個0x165700,
也能不用電腦就能估算個大概。
0x100 256bit,0x200 512bit,0x400 1K,0x800 2K
0x1000 就是4K,0x10000就是64K,0x100000 1024K
使用者空間裡的0x00001000-0x00400000 的Dos相容分區,現在還有用麼,按照書上的說明,程序堆,記憶體非配堆,都再0x00400000-0x10000000區域裡,那麼如果我們設定可執行檔案的加載位址從
0x00001000開始,是否程序空間就又能多出4M的記憶體區域可供使用呢?
一開始想速度看完第3章了解文法就能開始幹些"實事",結果直接跳到17章去看PE和PE程式才發現還是太多看不懂,意識到這本書不能跳躍式閱讀了,必須從頭開始打一個好基礎。
而且我也發現對windows32 API也相當的不熟悉,是以看這本書的時候是和"Windows程式設計","Windows核心程式設計"穿插閱讀,後兩本以前都看過,但是比較膚淺。這次學習彙編
在穿插看看加深下了解。
寫這個學習筆記一是想培養自己養成寫文檔的習慣,把閱讀後的重點都寫下來,既能加深記憶又能培養寫作能力,再者,也期望能有高水準的朋友能把我了解不正确的地方或者重點
的地方給個更好的指點,當然如果筆記能給和我一樣的初學者一個好的學習參考,那就更好了
windows下32位彙編語言學習筆記 第二章 準備程式設計環境
Win32可執行檔案的生成過程:
Win32下的可執行檔案就是常說的PE檔案。對于彙編來說,生成的步驟如下:
1.編輯.asm格式的彙編源代碼.
2.用ml.exe 将.asm源碼生成.obj檔案.
3.用link.exe 将.obj .rc(資源檔案,用于windows視窗界面定義的一種格式)連接配接成.exe檔案.
C API程式設計生成步驟和上面幾乎一樣。
1.編輯.C格式的源代碼。
2.用cl.exe(vs2008編譯器)或者gcc.exe(GCC編譯器),生成.ojb .o檔案.
3.用link.exe 連接配接成.exe 檔案。
其中的.rc是可選的,如果隻是簡單的建立一個視窗,使用預設的界面樣式,不需要編輯RC檔案。
因為我看這本書是結合"windows程式設計"和"windows核心程式設計"一起閱讀,是以在相關章節,我會結合後兩本書并把要點寫明。
開發環境的建立:
若要做其事,先要利其器,選好了工具學習使用起來就會更友善。
asm檔案編輯器的選擇
我使用的是Scite,一個開源的免費的,支援windows,linux的編輯器,支援N種種語言的文法高亮。
可自定義菜單功能。可設定不同語言的F1幫助,支援.chm .hlp .col(msdn幫助格式) F1鍵打開。
占用記憶體少,打開檔案速度快,優點數不勝數。以前一直是用NotePad++的,後來發現其作者居然在首頁上公開抵制奧運發表政治看法,就再不使用。
簡單說下配置方法,詳細的百度,google下自己搜尋。
1.配置編譯
Scite 裡預設的編譯快捷鍵是F7,打開asm.properties配置檔案照下面修改
我這裡下載下傳了個MASMPlus,是國人開發的一個彙編IDE,裡面就包含了所需要的連接配接編譯指令和windows .inc .lib 庫,其實這個編輯器也不錯。
#MASM路徑設定,
asmpath=D:/Program Files/MASMPlus
下面這個語句就是綁定快捷鍵盤F7的編譯指令
/I 參數是必須的,指定include 的路徑。其他的具體看書上的說明
command.compile.$(file.patterns.asm)=$(asmpath)/bin/ml.exe /I "$(asmpath)/include" /I "$(FileDir)" /c /coff /nologo /Fo $(FileName).obj $(FileNameExt)
#ctrl+ F7 連接配接指令
/LIBPATH 這個必須指定到Lib目錄
make.command=link
command.build.*.asm=$(asmpath)/bin/link.exe /LIBPATH:"$(asmpath)/Lib" /LIBPATH:"$(asmpath)/Exlib" /LIBPATH:"$(FileDir)" /SUBSYSTEM:WINDOWS /nologo /OUT:$(FileName).exe $(FileName).obj *.res
裡面用到了很多Scite自帶的變量必須 $(FileDir)檔案絕對路徑 $(FileName)檔案名 $(FileNameExt)檔案名帶擴充名,等等,也可以自定義,比如上面的asmpath就是我自定的路徑。
配置好以後,就可F7編譯,ctrl+F7連接配接了。絕對不建議使用radasm這樣的複雜IDE環境,功能是很強,但是大部分的功能初學者根本用不到。不要迷失在IDE環境中....
關于link
link有個參數 :BASE ,意思是把程式裝入指定的位址。上一章的時候說過0x00001000-0x00400000 是DOS相容的記憶體區域,不寫dos程式這塊位址浪費了可惜,看看能不能把裝入位址設定到這裡。
試下能不能裝到BASE:0x00010000位址,因為配置設定粒度是64K裝入位址必須是配置設定粒度的倍數,這裡剛好就是64K,也是整個記憶體空間的第一個配置設定粒度位址,這裡編譯的就是第二章的Test.asm
使用MASMPlus自帶的 link 5.12.8078 連接配接後提示:錯誤的windows 95位址...
LINK : warning LNK4096: /BASE value "10000" is invalid for Windows 95; image may not run
使用vs2008自帶的 link 9.00.30729.01
這次成功了,運作test.exe後檢視裝入位址,用Process Explorer導出看看,果然載入基址就是0x10000這個地方,後面的0x4000是大小.
Test.exe D:/My document/book/彙編/羅雲彬WINDOWS環境下32位彙編語言程式設計第2版(清晰+源碼)/附書CD光牒裡面的内容打包/Chapter02/Test/Test.exe 0x10000 0x4000
看來微軟也意識到,現在不會再有人開發dos相容程式了,0x400000以前這塊記憶體位址也不用留給16位程式了,編譯器也不再提示。
但如果不指定,預設還是裝入到0x00400000這個位址,也許大家都習慣了,微軟也不想随便就把首選裝入位址改掉。
但是有個問題,是不是首選位址往前移了0x3F0000就說明應用程式又多了3M多的記憶體尋址空間可以使用呢?
在試下往後最大能加載到什麼位址
我們知道,0x7FFF0000後面就是64K禁區,這裡就是使用者空間和核心空間的分界線。
先試下0x7FFE0000 ,可以連接配接,但是運作就提示
---------------------------
Test.exe - 應用程式錯誤
---------------------------
應用程式正常初始化(0xc0000018)失敗。請單擊“确定”,終止應用程式。
---------------------------
确定
---------------------------
這個初始化代碼我搜尋vs2008的所有頭檔案,在ntstatus.h裡查到下面的描述:
// {Conflicting Address Range}
// The specified address range conflicts with the address space. 指定的位址範圍與位址空間沖突?(我英文很差)
//
#define STATUS_CONFLICTING_ADDRESSES ((NTSTATUS)0xC0000018L)
實際上到了這個位址,就已經不讓使用了。雖然可以連接配接,但是運作就出錯,用了不該用的位址。
上一章說過,雖然64K禁區開始位址是0x7FFF0000但實際上從0x7FFE1000開始最後這段記憶體空間已經被标記成Reserve狀态的PAGENOACCESS,是以從0x7FFE開始後面的128K 記憶體尋址空間就已經不能用了
64K禁區增加到了128K,呵呵
最大的加載位址隻能是0x7FFD0000,這是最後一個可利用的配置設定粒度邊界。
綜上所述,win32 程式可以使用的記憶體尋址空間是:0x00010000-0x7FFD0000 這段區域,也就2G多一點。 傳說中4G的尋址空間實際能自己支配的隻用一半,呵呵,郁悶不?
特别注意下,link.exe可以使用vs2008自帶的最新的,ml.exe可不行,編譯就出錯,提示windows.inc裡的一段資料初始化失敗,可能是配套的inc不是新版的緣故。
寫到這裡我也發現MASMPlus自帶的inc和lib都比較老,從這裡開始就直接使用RadASM裡的inc和lib.ml.exe 和link.exe 就直接使用vs2008裡自帶的。
/STACK參數 格式:/STACK:reserve[,commit]
第一個reserve參數的數值是指保留的棧大小,第二個commit參數值指定送出的大小。
比如,用/stack,0x100000 指定保留1MB的棧大小,這是在記憶體空間裡保留出的堆棧大小,并不影響實體記憶體大小,因為還沒送出。
/stack,0x100000,0x100000 則既保留的同時也送出這塊記憶體,現在的實體記憶體已經被映射到這個記憶體空間,可以看見程序裡test.exe的記憶體大小已經從700多K長到1.7MB了
送出和保留的
保留隻是在記憶體空間位址上配置設定出指定的大小,準備使用,但還不能使用。可以通過 Process Explorer 檢視test.exe程序的虛拟記憶體增大。
送出是把保留出來的記憶體空間位址和實體記憶體做映射,送出以後才能正真使用這塊位址。這是後實體記憶體占用也增大了。
第一章曾今不确定預設棧大小,現在可以确定下來了,就是1MB,msdn裡關于link的/stack幫助裡明确寫到"The default stack size is 1 MB"
那麼棧保留在使用者空間的什麼位置?
msdn裡沒有說明,也可能我沒查到。我設定了不同的stack大小,用Cheat Engine 檢視記憶體空間情況(打開程序,點memory view 然後按 Ctrl + R)發現保留的位址總是在
0x00030000 這個記憶體空間位址上開始。這裡有2個問題
1.為啥從這裡開始 注:後面是純粹我自己的了解(沒找到相關參考),不對的地方請糾正。
記憶體空間是一塊連續的區域,棧是由系統維護的,一旦指定了大小程式中無法更改。系統的原則是盡量往記憶體的高位址處放,如果放到中間的位置,有可能會導緻出現記憶體空間出現碎片,
導緻下一次送出保留記憶體比較大時還需要系統移動這些在記憶體空間中間的保留段,為給新配置設定的空間騰地方,這個過曾會造成頻繁的和頁面檔案交換資料導緻系統變慢。
2.0x00001000-0x00400000 DOS相容記憶體區 真浪費了麼
明顯沒有,從棧的空間位址的配置設定上就看出,0x00030000剛開就在這個區域裡,這4M的空間其他的地方怎麼用的暫且不說,起碼預設1MB棧空間就用是這裡的記憶體空間。
如果把載入首先位址地設定成0x00010000 情況如何呢?
這時候棧的記憶體區域起始位址就往下增加了在0x00040000 這個位址上。
是以擔心系統會浪費程序僅可用的2G虛拟記憶體根本是沒有必要的,反而系統會充分幫助你的應用程式使用這塊空間。
RC編輯器
做複雜的界面就需要個RC資源編輯器了,放按鈕,設定圖檔等等,如果純寫代碼就比較麻煩了,可以下載下傳個ResEdit-1.4.4.19 資源編輯器,本書例子裡的RC資源可以直接用這個打開編輯。
可以再配置腳本裡設定一個編譯RC的快捷鍵
makepath=E:/compile/VC2008
command.name.1.*.rc=RC
command.1.*.rc=$(makepath)/bin/rc.exe /i "$(asmpath)/include" $(FileName).rc
這樣用scite 打開按Ctrl +1 就可以編譯成.res資源檔案了。
關于MAKE
在你的項目很複雜的情況下,每次ml後再 link就比較繁瑣,就可以通過編輯makefile 使用make幫助自動編譯。現在學習階段代碼都很小,沒有複雜的檔案關系,是以用不着這個東西。
主要是使用make必須寫makefile檔案,不能用參數解決,我現在還不知道如何用scite的配置自動生成模版檔案,還是暫時不用了。
幫助文檔
既然是windows32程式設計,用到的也都是win32API是以,msdn是必不可少的,這裡推薦大家去http://www.skygz.com/ 下載下傳msdn1.5綠色版,裡面還有vs2008綠色版。
要養成看幫助的習慣,而不要把代碼自動完成當做參考.
本章看起來比較輕松,重要的是自己手動搭配一個.asm 彙編的程式設計環境,還是那句話,不要用複雜的東西,進階的功能一個也用不到。大家可以用我推薦的Scite自己配置,
也可以使用MASMPlus。
windows下32位彙編語言學習筆記 第三章 使用MASM
本章講述的是masm 彙編的程式結構,基本文法,定義等,本章這些内容隻是彙編指令裡比較常用的,在下面的章節将要用到的指令。實際上彙編指令遠不止這些。感興趣可以參照其他的彙編書籍了解一下。不過對于本書下面的章節來說,這些指令基本上夠用了。
Win32彙程式設計式的基本結構
從例子可以看出來,Win32彙編的結構很簡單,下面簡單分析下。
模式定義
.386
.model falt,stdcall
option casemap:none
這個地方書上已經将的很清楚了。關于.386 .486 .586 .686 之類的指令集,我沒找到資料,試驗了一下寫成.686也沒什麼問題。
include includelib語句
include windows.inc
includelib kernel32.lib
這裡的include 和C語言裡的include 頭檔案一個道理,都是導入預先聲明好的函數,包括定義好的各種結構。
includelib 就是指定連接配接的時候告訴連接配接器從那個lib裡找你通過include引入并使用的函數,win32API都是以動态連結庫的形式提 供的,是以這裡就需要對你使用的winAPI包含在那個dll裡做到心中有數,不知道的就查msdn,每個API說明後面都有這個API包含在那個頭檔案 中,比如:
Header: Declared in Winuser.h; include Windows.h.
winAPI是C語言寫的,是以頭檔案都是.h的,彙編的頭檔案聲明是.inc的,打開kernel32.inc 找找Exitprocess 的申明 ExitProcess PROTO :DWORD
你也可以不用預定義的.inc頭檔案,自己定義。
如果你使用了函數确沒有包含對應的.lib,比如使用了ExitProcess函數,沒有includelib kernel32.lib,連接配接時就會報錯:
error LNK2001: 無法解析的外部符号 [email protected]
這個外部符号名就是你要調用的函數,名字很詭異吧,這裡先有個了解,講到調用約定的時候再詳細說明。
段定義,程式結束和入口.
.data ;全局變量段
szTest db '消息窗内容',0
szCaption db '消息窗标題',0
.code 代碼段
start:
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
end start
這裡的段就是PE格式裡的Section (區塊),一個PE檔案(可執行)最少包含2個區塊,代碼塊和資料塊,區塊的名隻是方面記憶,對于系統來說是無 關緊要的。區塊是按記憶體頁對齊的(0x1000 4K)。區塊的類型很多,比如.idata包含導入表,.rsrc,包含資源檔案等等。但是,任何時候不 要通過區塊名字來定位區塊,從PE結構的IMAGE_SECTION_HEADER來定位區塊才是正确的做法,因為區塊名字是可以任意的。關于PE結構的 說明,我見過的最詳細的就是“加密與解密第三版”第10章的介紹,大家可以去看看。
編譯本章的hello.asm 用OD打開exe可以看見有3個區段,.text .rdata 和 .data ,.text 就是代碼裡的.code 段,.data就是代碼裡的.data段,.rdata沒人定義怎麼自己冒出來了,其實這就是hello.exe的導入表,因為程式裡用到了2個外部 dll函數,MessageBox,ExitProcess。這個段就是編譯器自動生成的。至于為什麼叫.rdata,剛才說了,名字不是重要的,隻是幫 助記憶,導入表區段有的名字可能就是.idata。
另外還需要注意,程式的入口必須自己指定,彙編裡沒有Main這樣的程式執行起點,這點别忘了。
變量名,變量,資料結構
這個地方沒啥好說的,多看,多寫,慢慢就習慣了,值得注意的地方就是,變量的命名方式一定要按照後面代碼風格所說,按照匈牙利表示法來命名,從一開始就養成一個好習慣。
子程式,函數的定義和使用
調用約定和名稱修飾符
除了書上将的_cdecl,_stdcall等,還有一種c++builder裡常用的_fastcall調用,__fastcall調用也是被調用的函 數負責清棧,參數的傳遞規則是,從左邊開始不大于4位元組的參數分别用edx,ecx傳遞,其他參數遵循從又右到左的順序通過堆棧傳遞。
c c++在内部是通過函數修飾符來識别函數的,由編譯器在編譯時生成函數名稱修飾符,而且,不同的調用約定不同的語言生成的修飾符定義名稱不同,是以有必要了解一下函數的名稱修飾符。
例子函數:int max(int,int);
對于C語言
_cdecl調用
名稱修飾符是在函數前加一個下劃線:_max
_fastcall調用
名稱修飾符在函數前加一個@後面加一個@緊跟參數位元組數:@[email protected]
_stdcall調用
名稱修飾符在函數前加一個_後面加一個@緊跟參數位元組數:[email protected]
對于C++語言,不管任何調用約定,描述符都以?開頭後邊更函數名,然後是根據參數表查出的傳回值類型,然後是參數類型,最有以@Z結束
?+函數名+調用規則名+傳回類型+參數類型(從左到右)[email protected]
其中調用規則名表:_cdecl:@@YA,_stdcall:@@YG,_fastcall:YI
标示符:參數類型
X:void,D:char,E:unsigned char,F:short,H:int,I:unsigned int,J:long,K:unsigned long,M:float,N:double,_N:bool,U:Struct
指針:PA,const指針:PB
對于max函數修飾名稱就是:?max@@[email protected]。這裡給了個問号,意思就是不同的調用規則就更具調用規則表變化,其他不變。
很明顯,C++的修飾更為詳細。
現在回過頭看看剛才的錯誤提示:error LNK2001: 無法解析的外部符号 [email protected]
__imp_ 這個是代表函數ExitProcess是從外部導入的,後面的[email protected]很明顯參數是四位元組的和ExitProcess(UNIT uExitCode)相符
實際上對于C++的類成員函數,描述符的規則又有不同,但是,如果你寫的DLL動态連結庫使用自定義類,估計沒人會用的,使用類了就不能通用了。
MASM的優化
都知道彙編效率高,但是MASM編譯出的EXE真的就是最佳優化的麼?讓我們看看本章中的hello.exe 用OD反彙編看看是不是這樣。
反彙編内容:
00011000 >/$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00011002 |. 68 00300100 PUSH Hello.00013000 ; |Title = "A MessageBox !"
00011007 |. 68 0F300100 PUSH Hello.0001300F ; |Text = "Hello, World !"
0001100C |. 6A 00 PUSH 0 ; |hOwner = NULL
0001100E |. E8 07000000 CALL <JMP.&user32.MessageBoxA> ; /MessageBoxA
00011013 |. 6A 00 PUSH 0 ; /ExitCode = 0
00011015 /. E8 06000000 CALL <JMP.&kernel32.ExitProcess> ; /ExitProcess
0001101A $- FF25 08200100 JMP NEAR DWORD PTR DS:[<&user32.Mess>; user32.MessageBoxA
00011020 .- FF25 00200100 JMP NEAR DWORD PTR DS:[<&kernel32.Ex>; kernel32.ExitProcess
看看那2個CALL,一個調用MessageBoxA,一個調用ExitProcess,這個JMP産生了額外的代碼,并且增加執行時間,産生這樣的代碼 是因為編譯器不知道你調用的函數是從外部導入的。如果編譯器預先知道這個函數是從外部引入的,編譯器就會把CALL後面的位址直接指向,PE檔案的 IAT(import_address_table)輸入表中的函數位址,當程式運作時由系統加載器更新IAT表(如果需要的話),這樣就調用了函數在 DLL中的正确位址,避免了這種低效能的調用方式。
進階語言,比如C語言在引入外部DLL函數時,再dll頭檔案裡對于每一個函數都有一個描述 __declspec(dllimport),這就是告訴編譯器,這個函數是從外部引入的,進而提高空間和時間效率。
看看C寫的,功能呢個同樣的代碼,編譯後的反彙編内容:
00401000 /$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00401002 |. 68 00304000 PUSH HelloMsg.00403000 ; |Title = "HelloMsg"
00401007 |. 68 0C304000 PUSH HelloMsg.0040300C ; |Text = "Hello, Windows 98!"
0040100C |. 6A 00 PUSH 0 ; |hOwner = NULL
0040100E |. FF15 AC204000 CALL NEAR DWORD PTR DS:[<&USER32.MessageBoxA>] ; /MessageBoxA
00401014 |. 33C0 XOR EAX, EAX
00401016 /. C2 1000 RETN 10
這個MessageBoxA的CALL才是效率最高的call!
但是悲劇的是在masm裡我們無法用任何描述告訴編譯器,目前使用的函數是從外部引入的。結果就是使用效率最高的語言确産生了效率最低的外部函數調用...
有沒有辦法解決,确實有,我google了一下,發現了一段代碼。
比如我們調用ExitProcess函數,可以預先這樣寫
[email protected] TYPEDEF PROTO STDCALL :DWORD ;定義一個新的類型[email protected]
EXTERNDEF STDCALL [email protected]:PTR [email protected] ;定義一個外部變量,類型為上面定義的類型
ExitProcess EQU <[email protected]> ;定義一個符号ExitProcess
或者:
externdef [email protected]:PTR :DWORD
;;為了在用ExitProcess不必寫成[email protected]這種複雜形式
;;是以等值定義
Exitprocess equ <[email protected]>
把上面3行代碼加到 模式定義後面,注釋掉include 'kernel32.inc',重新編譯,現在看反彙編的内容:
00011000 >/$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00011002 |. 68 00300100 PUSH Hello.00013000 ; |Title = "A MessageBox !"
00011007 |. 68 0F300100 PUSH Hello.0001300F ; |Text = "Hello, World !"
0001100C |. 6A 00 PUSH 0 ; |hOwner = NULL
0001100E |. E8 09000000 CALL <JMP.&user32.MessageBoxA> ; /MessageBoxA
00011013 |. 6A 00 PUSH 0 ; /ExitCode = 0
00011015 /. FF15 00200100 CALL NEAR DWORD PTR DS:[<&kernel32.Ex>; /ExitProcess
0001101B CC INT3
0001101C $- FF25 08200100 JMP NEAR DWORD PTR DS:[<&user32.Mess>; user32.MessageBoxA
看見沒ExitProcess的調用彙編代碼成了最佳調用了。
解釋這三行或兩行的原理:
當一個庫形成後,内部都是代碼的集合,如果沒有一個清單來訓示他們内部的結構,将無法使用庫,這個庫的清單就是Include檔案,當然如果事先知道庫中有哪個函數,也可以自己建立這個清單,建立清單有講究的,必須指定externdef來告訴編譯器,在使用一個外部函數,否則就會多一次跳轉,當然如果将函數申明為外部的函數,在PE檔案中就看不到一個類似輸入表的資料段,不是導入表,這裡有排列jmp near dword ptr ds : [00400xxx..],這個就是跳轉到一個RVA,那裡儲存着函數的相關資訊,将函數申明為外部的函數,這個跳轉就沒有了,就直接call哪個RVA了。
我還發現了一個網站 http://www.japheth.de/JWasm.html 這個網站提供了一套自己修改過的.inc檔案,而且使用整個代碼裡隻需要include 他們的windows.inc檔案。
編譯生成後就是優化了的call代碼。
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.686
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 檔案定義
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
includelib user32.lib
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 資料段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data
szCaption db 'A MessageBox !',0
szText db 'Hello, World !',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代碼段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
start:
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
編譯後的反彙編:
00401000 >/$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00401002 |. 68 00304000 PUSH Hello.00403000 ; |Title = "A MessageBox !"
00401007 |. 68 0F304000 PUSH Hello.0040300F ; |Text = "Hello, World !"
0040100C |. 6A 00 PUSH 0 ; |hOwner = NULL
0040100E |. FF15 08204000 CALL NEAR DWORD PTR DS:[<&user32.Mess>; /MessageBoxA
00401014 |. 6A 00 PUSH 0 ; /ExitCode = 0
00401016 /. FF15 00204000 CALL NEAR DWORD PTR DS:[<&kernel32.Ex>; /ExitProcess
2個call完全優化了。
其實要想到達這個效果,并非一定要這樣做,因為這些事都是編譯器或連結器幹的,在MASM32的軟體SDK中有個叫poasm的編譯器,他有個polink,用這個連結器時,将節區合并,就會自動将輸入的那個清單去掉,不是導入表。
windows下32位彙編語言學習筆記 第四章 第一個視窗程式 1 (windows的消息機制)
FirstWindow程式代碼很簡單,隻有一個地方要說下
_WinMain 函數裡的下面2行代碼, 把目前程序句柄指派給WNDCLASSEX的hInstance,這裡不能使用mov @stWndClass.hInstance,hInstance,因為mov指令不支援2塊内從間的直接指派。
是以先把hInstance壓棧再彈出到@stWndClass.hInstance
push hInstance
pop @stWndClass.hInstance
當然也可以這樣做
mov ecx,hInstance
mov @stWndClass.hInstance,ecx
WIN32的消息機制
windows系統是一個消息驅動的OS,操作通過處理各種消息來響應使用者的操作。從第一個windows程式就可以看出來,大部分的代碼都是處理消息的。要開發windows程式,不管你用什麼開發工具什麼語言,掌握消息機制的原理都是非常必要的。
對于每一個帶有視窗的線程,系統都會給他非配一個自己的消息隊列,用于處理消息派送(Dispatch)。每個線程都用自己的消息循環來接受消息。每個線程列隊預設管理最大10000個消息,修改系統資料庫下面的鍵值可以修改列隊中的消息數。建議的最小值是4000
HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/Windows/USERPostMessageLimit.
線程列隊不是一個公開的資料結構(THREADINFO),其中包括登記消息隊列(Posted-message queue),消息發送隊列 (Send-message queue),消息應答隊列(reply-message queue),虛拟輸入隊列(virtualized- input queue),喚醒标志(wake flag),以及用來掃描線程局部輸入狀态的若幹變量。(Windows核心程式設計)
消息列隊提取優先級
1.檢查QS_SENDMESSAGE 标志 GetMessage 不處理Send消息,如果隊列中沒有其他send消息,關閉QS_SENDMESSAGE标志,GetMessage()不傳回檢查其他消息。
2.檢查QS_POSTMESSAGE 标志 GetMessage 從此列隊取出消息處理并由DisPatch分發到指定視窗回調函數處理。GetMessage傳回True,沒有其他post消息關閉标志。
3.檢查QS_QUIT 标志 如果被PostQuitMessage()打開,則GetMessage傳回False退出消息循環,并且關閉QS_QUIT标志
4.檢查QS_INPUT 标志 GetMessage 從此列隊取出消息處理由TranslateMessage()處理鍵盤滑鼠消息,然後由DisPatch分發到指定視窗回調函數處理沒有其他消息關閉标志.
5.檢查QS_PAINT 标志 處理同2
6.檢查QS_TIME 标志 首先複位計時器,GetMessage傳回True,如果沒有計數器,關閉QS_TIME标志。
優先級很清楚,send優先級最高,最低的是time。
Windows定義了很多消息都以WM_開頭,都是用#DEFINE 定義的常量,使用者可以定義自己的消息,windows規定使用者的消息從WM_USER 0x0400開始。
BOOL PostMessage(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam);
往程序的消息列隊發送消息PostMessage,這個函數往指定程序的消息列隊發送一個消息,發送完畢立即傳回。調用函數無法知道發送的消息是否能被處理。如果這個指定視窗未處理完自己消息列隊的所有消息前就推出了,就會處理不到post的消息。
PostMessage發送的消息參數不能包含指針參數,MSDN的說明是:
“如果發送一個低于WM_USER範圍的消息給異步消息函數 (PostMessage.SendNotifyMessage,SendMesssgeCallback),消息參數不能包含指針。否則,操作将會失 敗。函數将再接收線程處理消息之前傳回,發送者将在記憶體被使用之前釋放。”
我的了解是,就算目标程序知道你發來的是個指針位址,但是2個程序之間的尋址空間是獨立的,互相不可通路,怎麼能擷取發送程序記憶體空間裡的資料呢?
可以試下,把例子Send.asm修改一下看看。
WM_USER 值是 0x0400,使用者自定義消息從這裡開始,前面的是windows保留的消息。這裡必須把WM_SETTEXT消息改成自定義消息,否則PostMessage根本不會發送WM_SETTEXT消息給目标線程。
invoke PostMessage,hWnd,WM_USER + 1,0,addr szText
Receive.asm修改接受WM_SETTEXT消息的代碼改成這樣:
eax == WM_USER + 1
invoke MessageBox,hWnd,addr lParam,addr szCaptionMain,MB_OK
用OD打開Receive.exe 運作在 0040102B處下段,運作send.exe,發送之前提示的位址(00402072)到了Receive,windows幹脆直接把你傳遞的指針位址當做DWORD類型處理了,顯示 "r @",
是00402072的ASCII碼值,就算當成指針處理,00402072也是指向Receive記憶體空間裡的,不可能是指向send記憶體空間裡的資料。
關于WM_QUIT消息
視窗回調函數裡不可能接到WM_QUIT消息。因為從消息列隊裡GetMessage()收到WM_QUIT消息 就傳回0,消息循環就會結束,是以DispatchMessage()也不可能再把這個消息分發到視窗的回調函數。這就是為什麼,書裡一再強調要在 WM_DESTORY的消息事件裡加上PostQuitMessage()的原因。如果不加,程式隻是銷毀視窗,但是程序任然存在。消息循環還在運作,但 是因為視窗已經銷毀,是以他不可能再從消息列隊裡取得任何消息。
使用PostQuitMessage()與PostMessage()發送消息的不同
前者把消息列隊裡的QS_QUIT标志打開,并且等待程式處理完消息列隊裡的所有消息後才結束消息循環。
後者是把WM_QUIT直接放到消息列隊,消息循環取到得下一個消息是WM_QUIT就立即退出。MSDN裡不建議使用PostMessage發送WM_QUIT消息,因為這樣會造成程式的收尾工作無法進行,正常退出後所需要的資源釋放等操作就沒法執行了。
用SendMessage無法發送WM_QUIT消息,因為SendMessage并不是吧消息放入消息列隊,是以,GetMessage根本無法得到SendMessage發送的消息。
BOOL PostThreadMessage(DWORD dwThreadId,UINT uMsg,WPARAM wParam,LPARAM lParam);
這個函數和PostMessage類似,都是發送完消息立即傳回,不同的是這個函數向指定的ThreadId發送 一條消息。這個函數發送的消息不回被配置設定到目标程序視窗的回調函數,因為當消息放入列隊時,MSG的hwnd被設定為NULL,沒有視窗句 柄,DispatchMessage能把消息配置設定給誰呢?PostThreadMessage也可以發送WM_QUIT消息,消息會放到隊列的尾端。在 qs_input之前處理該消息。
PostMessage 和PostThreadMessage發送WM_QUIT消息都會造成視窗的首尾代碼無法執行。用的時候需要注意下。
LRESULT SendMessage(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
SendMessage同步發送消息,發送程序要等待目标程序視窗的回調函數處理完成消息後才能恢複運作,調用點線程在等待SendMessage傳回的過程中是挂起狀态,本身也無法響應任何操作。
發送程序再等待的過程中,如果系統中其他的程序向等待程序發送消息,則發送程序立即處理消息。
Windows提供了其他的4個API來進行程序間的消息發送。
LRESULT SendMessageTimeout(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam,UINT fuFlags,UINT fuTimeout,PDWORD_PTR pdwResult);
fuFlags參數由下列标志組成
SMTO_ABORTIFHUNG 如果目标程序處于挂起狀态立即傳回。
SMTO_BLOCK 發送程序在SendMessageTimeout傳回之前不處理任何消息
SMTO_NORMAL 0值,如果不适用其他标志,就是用這個标志
SMTO_NOTIMEOUTIFNOTHUNG 如果目标程序未處于挂起狀态不考慮fuTimeout限定等待值
fuTimeout參數指定等待時間機關毫秒
pdwResult 指向一段記憶體區域,儲存傳回結果。如果用SendMessageTimeout本身線程的視窗則直接調用視窗的回調函數,并且将結果儲存在pdwResult中。
BOOL SendMessageCallback(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam,SENDASYNCPROC lpCallback,ULONG_PTR dwData);
lpCallback 參數 指向一個CALLBACK函數,定義如下
VOID CALLBACK ResultBack(HWND hwnd,UINT uMsg,ULONG_PTR dwData,LRESULT lResult);
發送線程使用SendMessageCallback發送消息到接受線程的發送消息列隊,并了解傳回。當接收線程處理完消息後,用一個消息登記到發送線程 的應答消息隊列,然後系統調用ResultBack函數通知發送程序。前2個參數是接受線程視窗的句柄,消息值,第三個參數dwData就是 SendMessageCallback中最後一個參數,lResult參數是接受消息視窗回調函數的傳回值。
接收程序處理完SendMessageCallback函數後先在發送程序消息列隊登記應答消息,發送程序在下一次調用GetMessage,PeekMessage時,執行ResultBack函數
Bool SendNotifyMessage(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam);
SendNotifyMessage将消息放到接收線程的發送消息列隊(QS_SENDMESSAGE)中,并且立即傳回。和PostMessage類似,但不同的是。
發送消息列隊的優先級比登記列隊(QS_POSTMESSAGE)的優先級高。是以SendNotifyMessage發送的消息比PostMessage發送的消息處理的早。
向程序發送視窗消息時,SendNotifyMessage效果和SendMessage完全一樣,等待消息處理完之後才傳回。
BOOL ReplyMessage(LRESULT lResult);
這個函數是用于接收線程視窗的回調函數中,調用ReplyMessage後,發送線程恢複運作。
判斷消息類型
BOOL InSendMessage();
如果目前消息是程序間消息,傳回TRUE,如果是程序内消息傳回FALSE;
DWORD InSendMessageEx(PVOID pvReserved);
這個函數傳回正在執行的消息類型。傳回值如下:
ISMEX_NOSEND 消息是線程内部消息
ISMEX_SEND 消息是用SendMessage或SendMessageTimeout發送的程序間消息
ISMEX_NOTIFY 消息是SendNotifyMessage發送的程序間消息
ISMEX_CALLBACK 消息是SendMessageCallBack發送的程序間消息
ISMEX_REPLIED 消息是是程序間消息,并且已經調用ReplyMessage
消息隊列的狀态标志
DWORD GetQueueStatus(UINT fuFlags);
參數fuFlags是由一組标志聯合起來的值,用來檢視特定的喚醒隊列标志。
QS_KEY WM_KEYUP、WM_KEYDOWN、WM_SYSKEYUP或WM_SYSKEYDOWN
QS_MOUSE MOVEWM_MOUSEMOVE
QS_MOUSEBUTTON WM_?BUTTON*(其中?代表L、M或R、*代表DOWN、UP或DBLCLK)
QS_MOUSE 同QS_MOUSEMOVE|QS_MOUSEBUTTON
QS_INPUT 同QS_MOUSE|QS_KEY
QS_PAINT WM_PAINT
QS_TIMER WM_TIMER
QS_HOTKEY WM_HOTKEY
QS_POSTMESSAGE 登記的消息(不同于硬體輸入事件)。當隊列在期望的消息過濾器範圍内沒有登記
的消息時,這個标志要消除。除此之外,這個标志與QS_ALLPOSTMESSAGE相同
QS_ALLPOSTMESSAGE 登記的消息(不同于硬體輸入事件)。當隊列完全沒有登記的消息時(在任何消息
過濾器範圍),該标志被清除。除此之外,該标志與QS_POSTMESSAGE相同
QS_ALLEVENTS 同QS_INPUT|QS_POSTMESSAGE|QS_TIMER|QS_PAINT|QS_HOTKEY
QS_QUIT 已調用PostQuitMessage。注意這個标志沒有公開,是以在WinUser.h檔案中沒有。它由系統在内部使用
QS_SENDMESSAGE 由另一個線程發送的消息
QS_ALLINPUT 同QS_ALLEVENTS|QS_SENDMESSAGE
消息類型存放在回值的高位元組中(2個位元組),低位元組儲存還沒有處理的消息類型。
上面幾個函數都是用來發送消息,很多函數不是常用的,但多了解幾個函數沒有壞處,了解的東西越多,遇到問題解決的辦法也就越多。
鍵盤,滑鼠消息
windows程式與使用者的互交都是通過滑鼠鍵盤實作的,是以必須要了解windows是如何處理鍵盤滑鼠消息的。
首先,發生的鍵盤滑鼠消息是先報錯在系統消息列隊的(不是直接發放到應用程式列隊),當應用程式處理上一個輸入消息後,系統消息隊列才把下一個輸入消息投 放到應用程式列隊。因為如果按鍵的輸入速度比應用程式處理速度快,後來的鍵如果還是發往目前的焦點視窗句柄,那麼切換到新視窗後後來輸入的鍵還是會發送到 先前的視窗,直到上一個視窗處理完所有的未處理的按鍵消息,按鍵才會改變發送的視窗句柄到新視窗。
其次,每一個按鍵産生2類消息,按鍵消息和字元消息。很顯然,有的按鍵隻有按鍵消息沒有字元消息,比如Capslk,Shift等。
按鍵又分為系統按鍵和非系統按鍵,對于系統按鍵,當按下一個鍵發生WM_SYSKEYDOWN消息,放開這個鍵發生WM_SYSKEYUP消息,對于非系統間,按下和放開發生WM_KEYDOWN和WM_KEYUP消息。
很顯然這些消息都成成對出現的。一個KEYDOWN,接着就是一個KEYUP,
對于系統按鍵,通常是windows系統本身比較關心的消息,系統按鍵通常由ALT快捷鍵産生,Alt tab Alt F4 Alt esc 等等。應 用程式不處理ALT消息,而是交給DefWindowProc來處理,這就說明應用程式的菜單快捷鍵也是由系統處理。系統将Ctrl+s這類的快捷鍵,轉 換成菜單指令消息,不用自己去處理。
對于所有的4類按鍵消息WM_SYSKEYDOWN WM_SYSKEYUP WM_KEYDOWN WM_KEYUP,wParam參數儲存虛拟鍵代碼,LParam參數包含按鍵的其他資料。
産生虛鍵代碼的原因是因為早期的鍵碼是由真實鍵盤産生,叫做"掃描碼",掃描嗎是按照鍵盤的排列順序産生的,比如16 是Q,17是W(數數看,呵呵)很明顯這種鍵碼會因為鍵盤布局的變化而變化,太過于裝置話,于是通過定義虛拟鍵代碼。
虛拟代碼是一些列VK開頭的定義在winuser.h裡的值。例如VK_TAB,VK_RETURN(Enter鍵)等等,鍵盤數字0-9和字母a-z,A-Z 就是ASCII的值。小鍵盤上的數字是VK_NUMPAD0-VK_NUMPAD9,其他的功能鍵都是VK_+鍵的英文含義組成。
lParam參數的32位分成6個字段,用于表示不同的消息
00-15位, 包含按鍵的從重複次數。
16-23位, 包含按鍵的OEM掃描嗎,上面說過掃描嗎。
24 位, 包含按鍵的擴充标志,這個标準被windows忽略不用
29 位, 包含按鍵的内容代碼,對于系統鍵盤此位是1,對于非系統鍵此位為0
30 位, 包含按鍵的先前狀态,如果鍵是先前釋放的,為0,否則為1.
31 位, 包含按鍵的轉換狀态,如果鍵盤按按下,為0,否則為1。
25-28位未知。
short GetKeyState(int vKey)函數用來獲得某個鍵到目前為止的狀态,比如判斷shift是否按下 GetKeyState(VK_SHIFT),按下高位時1,否則是0,GetKeyState(VK_CAPITAL)(Capslk鍵)如果打開低位 傳回1,注意這個GetKeyState傳回short類型的值16位,不是上面說的LParam的值。GetKeyState不是實時檢查狀态的,指檢 查到目前為止的鍵盤狀态。
short GetAsyncKeyState(int vKey)函數用來擷取目前的某個鍵的目前狀态。高位為1則目前判斷的建被按下,低位傳回1則,則按鍵在上次調用GetasyncKeystate以來狀态是被按下的。
GetKeyState判斷組合鍵比較合适,因為可以判斷某個鍵到目前為止的狀态,按下了Ctrl再按下S,那麼可以在S鍵的處理消息上判斷GetKeyState(VK_LCTRL)是否按下。
GetAsyncKeyState可以用來做個循環,當某個鍵現在按下,處理某些事情。
字元消息
每當一個鍵被按下,就産生一個按鍵消息和字元消息,通常我們隻關心字元消息,因為同樣的按鍵産生的字元有可能是不同的,比如,打開搜狗輸入法按Shit + 4打出的字元是¥,關閉輸入法打出的是$。
字元消息的wParem參數是按鍵的ASCII值,是以在回調函數中可以if (wParam == 'a')這樣判斷輸入的字元。
滑鼠消息
滑鼠按鍵全使用消息,每個鍵有3個消息,BUTTONDOWN,BUTTONUP,BUTTONDBLCLK(輕按兩下),WM_L(左鍵) WM_M(中鍵) WM_R(右鍵)加上三個消息代表了滑鼠顯示區消息。
滑鼠的移動消息是WM_MOUSEMOVE
此時wParam參數表示下列的按鍵是否被按下MK_CONTROL MK_LBUTTON MK_MBMTTON MK_RBUTTON MK_SHIFT
lParam低位代表滑鼠X坐标,高位代表滑鼠Y坐标。
可以看出SendMessage()發送的WM_CHAR消息不會被目标程序的視窗回調函數處理,因為SendMessage直接發送到回調函數,沒有經過TranslateMessage翻譯鍵盤消息。
線程間的資料共享
WM_SETTEXT消息
首先說明WM_SETTEXT消息不是一個用來做程序間發送資料用的,這個消息是用來設定視窗标題,或者按鈕文 本,或者Edit控件内容的。比如SetWindowText(HWND hwnd,LPCTSTR lpString)(設定視窗的标題)調用這個函數 實際上就是産生了一個WM_SETTEXT消息,通常由預設回調函數DefWindowProc來處理。想想就行了,隻能發送一個字元串有什麼用?
但是WM_SETTEXT消息特殊的地方就是,系統為用這個消息發送的字元串開辟另外一塊共享記憶體映射空間,使不同程序接收消息的線程也能夠收到并且使用這個字元串。
對應的還有一個WM_GETTEXT消息,這個消息是從目标視窗句柄傳回字元串資訊,同樣 GetWindowText(HWND hwnd,LPCTSTR lpString,int iMaxCount)函數也是産生一個 WM_GETTEXT消息由DefWindowProc來處理,參數中多了一個iMaxCount用來表示字元串的長度。
WM_COPYDATA消息
WM_COPYDATA消息把自定義的一塊資料發送到目标線程,目标程序的視窗回調函數中必須有這個消息的處理方法,否則發了也沒用。
WM_COPYDATA WMSETTEXT這兩個資料傳遞消息都隻能使用SnedMessage()發送,SnedMessage傳回了系統就會釋放開辟的記憶體空間,用其他的方法發送,系統不知道目标程序什麼時候處理消息,是以也無法釋放記憶體映射空間。
可以在Send.asm裡加入下面代碼看看如果用PostMessage傳回什麼錯誤提示。
lpBuffer db 512 dup (?) ;先定義一個buffer
invoke PostMessage,hWnd,WM_COPYDATA,0,addr stCopyData ;試用PostMessage發送,根本就沒有發送消息
.if eax == 0
invoke GetLastError
invoke FormatMessage,FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_IGNORE_INSERTS,NULL,
eax,LANG_NEUTRAL,offset lpBuffer,sizeof lpBuffer,NULL
invoke MessageBox,NULL,offset lpBuffer,offset szCaption,MB_OK
.endif
這個方法是使用GetLastError函數先獲得上一次調用函數失敗的代碼,然後通過FormatMessage找到錯誤代碼的描述,參數裡設定說明是中文。
大部分的winAPI在調用失敗後都可以通過GetLastError獲得調用失敗的錯誤代碼。這個方法很好用,可以及時了解為什麼出錯。
本章關于彙編本省的東西不多,但是既然是windows的程式設計,必須先要了解一些windows程式設計一些基本的知識。特别是消息機制,這是整個windows程式設計的基礎。
我參考了核心程式設計,windows程式設計,把windows的消息機制先做個介紹,了解了這些,再去看本章的代碼和2個消息發送例子就輕車熟路了。
本章将分為2個部分做筆記,第一部分消息機制,第二部分準備做一些反彙編方面的實驗,比如比較C寫的視窗,和彙編寫的視窗傳回編的結果為何不同,比如如何找到并且攔截消息等等,當然這隻是想法,能不能有結果還不一定。
鍵盤消息的使用
可以使用PostMessage給目标視窗或者控件發送鍵盤消息,按鍵消息和字元消息,但是使用SendMessage隻能發送字元消息,而不能發送按鍵消息,想想為什麼?
開始練習按鍵消息前,必須要先了解2個函數:
HWND FindWindow(LPCTSTR lpClassName,LPCTSTR lpWindowName);通過lpClassName窗 口注冊類名(就是WNDCLASS裡的lpszClassName名稱)或者lpWindowName視窗标題名獲得視窗句柄。
2個參數随便用一個就可以,不使用的給NULL。
HWND FindWindowEx(HWND,hwndParent,HWND hwndChildAfter,LPCTSTR lpszClass,LPSTSTR lpszWindow);這個函數可以通過視窗句柄和控件類名或者控件标題名獲得這個控件的句柄。
先通過FindWindow得到主視窗句柄,然後通過FindWindowEx得到主視窗内某個控件的句柄。
下面看看如何通過PostMessage給windows記事本發送按鍵消息
首先找到記事本
szClac db 'Notepad',0 記事本主窗體的類名,可以通過Spy++擷取
szEdit db 'Edit',0 内容用于寫内容的Edit控件
hwndnote db ? 用于儲存句柄
invoke FindWindow,offser szCalc,NULL ;找到記事本句柄
invoke FindWindowEx,eax,NULL,offset szEdit,0 ;找到edit控件的句柄
mov hwndnote,eax
下面就可以給記事本發送各種鍵盤消息,比如
invoke SendMessage,hwndnote,WM_KEYDOWN,VK_1,0 ;發送一個按鍵消息1
invoke PostMessage,hwndnote,WM_KEYDOWN,VK_2,0 ;發送一個按鍵消息2
invoke SendMessage,hwndnote,WM_CHAR,VK_3,0 ;用SendMessage發送一個字元消息3
想象發送後記事本上的的字元順序是1,2,3麼?
發送一個組合鍵Alt+E,就是打開記事本的編輯菜單
invoke PostMessage,hWndnd,WM_SYSKEYDOWN,VK_MENU,020000001h ALT鍵按下
invoke PostMessage,hWndnd,WM_SYSKEYDOWN,VK_E,020000001h E鍵按下必須要把第29位設定成1,代表alt鍵已經下
invoke PostMessage,hWndnd,WM_SYSCHAR,VK_E,020000001h 發送一個系統字元E
invoke PostMessage,hWndnd,WM_SYSKEYUP,VK_E,080000001h E鍵放開,必須把31位設定成1,表示這個是系統鍵
invoke PostMessage,hWndnd,WM_KEYUP,VK_MENU,080000001h ALT鍵放開,31位系統鍵設定成1
這組消息可以通過SPY++監視記事本的鍵盤輸入狀态得到,其實可以精簡,隻用下面2條就可以。
invoke PostMessage,hWndnd,WM_SYSKEYDOWN,VK_E,020000001h E鍵按下必須要把第29位設定成1,代表alt鍵已經下
invoke PostMessage,hWndnd,WM_SYSKEYUP,VK_E,080000001h E鍵放開,必須把31位設定成1,表示這個是系統鍵
因為E鍵的lParam參數的29位置1,已經說明這個E在這裡表示系統按鍵,29位置1表示ALT鍵已經按下。
按鍵彈起的時候,必須把31位置1,表示這是個系統鍵彈起。否則會當做普通鍵,并且在記事本裡列印出字母e。
現在想出來這組消息後,記事本上會是什麼字元麼?答案是:321,前面說過SendMessage的優先級高于PostMessage,是以是先打出3,然後是1,最後是2。
關于windows消息的操作還有很多,這裡隻舉出了最基本的發送鍵盤消息的方法。了解這些基本的操作是位日後學習使用其他消息操作打下一個好的基礎。
滑鼠消息的使用
鍵盤消息隻發送給目前擁有輸入焦點的視窗,滑鼠消息不同,隻要滑鼠達到,視窗就會收到滑鼠消息。當滑鼠在視窗顯示區域内,滑鼠消息的lParam參數是滑鼠所在視窗的X,Y坐标值,當滑鼠不在視窗顯示區域内,參數lParam是桌面的X,Y坐标值。
顯示區域:是指使用者能夠輸出顯示資訊結果的區域。非顯示區域是指:菜單,标題欄,滾動條
對于顯示區内發送鼠按鍵消息,wParam參數指定滑鼠按鍵以及Shift和Ctrl按鍵的狀态,鍵值如下:
MK_CONTROL 表示ctrl按下 MK_?BUTTON 表示滑鼠3個鍵按下 MK_SHIFT 表示shift按下
lParam參數指定滑鼠的坐标值,高位Y坐标,低位X坐标
下面的例子代碼是使用鍵盤的上下左右方向鍵移動滑鼠光标,空格鍵發送滑鼠單擊消息。可以把SendMessage句柄改成“畫圖”程式句柄,這樣在目前視窗按空格鍵,将會在畫圖程式的同樣位置畫出一個點。
_MoveMouse proc hwnd,wParam,lParam
local @szPos [128]:byte
local @stPoint:POINT
local @stRect:RECT
invoke GetCursorPos,addr @stPoint ;獲得目前滑鼠螢幕坐标位置
invoke ScreenToClient,hwnd,addr @stPoint ;将滑鼠的螢幕坐标位位置轉換成目前視窗内的坐标位置
invoke wsprintf,addr @szPos,offset szMsg,@stPoint.x,@stPoint.y
invoke SetWindowText,hwnd,addr @szPos
mov eax,wParam
.if eax == VK_LEFT
sub @stPoint.x,1
.elseif eax == VK_RIGHT
add @stPoint.x,1
.elseif eax == VK_UP
sub @stPoint.y,1
.elseif eax == VK_DOWN
add @stPoint.y,1
.elseif eax == VK_SPACE
mov eax,@stPoint.y
shl eax,16
add eax,@stPoint.x
invoke PostMessage,hwnd,WM_LBUTTONDOWN,MK_LBUTTON,eax
invoke PostMessage,hwnd,WM_LBUTTONUP,0,eax
.endif
invoke ClientToScreen,hwnd,addr @stPoint ;将目前視窗坐标位置轉換成螢幕位置
invoke SetCursorPos,@stPoint.x,@stPoint.y ;設定光标位置
ret
_MoveMouse endp
在視窗的回調函數中加入以下代碼:
.elseif eax == WM_KEYDOWN
mov eax,wParam
.if wParam == VK_LEFT || wParam || VK_RIGHT || wParam == VK_UP || wParam == VK_DOWN || wParam == VK_SPACE
invoke _MoveMouse,hWnd,wParam,lParam
.endif
對于非顯示區滑鼠消息和顯示區滑鼠消息類似,消息後加"NC"代碼表示非顯示區消息,例如WM_NCLBUTTONCLICK
參數wParam是一些定義在winuser.h裡以HT開頭的的非顯示區域代碼,比如HTCAPTION 代表标題欄,HTCLOSE,代表視窗右上角的關閉按鈕等等。
參數lParam表示螢幕坐标,不是顯示區坐标,同樣低位是X坐标,高位是Y坐标。
純C寫的FirstWindow和彙編FirstWindow的差別
同樣的FirstWindow程式,我用C寫了一個,反彙編後比較,發現反彙編結果裡多了很多編譯器添加的代碼。尺寸也大了不少,查了一些資料,發現原來這些編譯器添加的代碼就是傳說中的CRT,C語言運作時環境。
用C寫windows程式,都知道程式從winMain開始執行,實際上在這之前,是有其他的函數來調用WinMain的。這個函數就叫做入口函數。
入口函數對運作時庫和程式運作環境進行初始化,包括堆,I/O,線程等等。入口函數執行完後才回去調用main函數正式開始執行程式,WinMain執行完後,傳回到入口函數,由入口函數進行清理工作。
這倒也好了解,winMain之前肯定有些東西執行了什麼,比如winMain的4個參數,hInstance,szCmdLine,iCmdShow 都是從啟動函數傳給winMain的。
對于我現在使用的vs2008的編譯器來說,入口函數的代碼位于srt/src/crt0.c檔案裡。函數的名稱是__tmainCRTStartup。現在看看裡面都幹了些什麼關鍵:
首先定義了個STARTUPINFO StartupInfo結構,使用GetStartupInfo(&StartupInfo)初始化。STARTUPINFO結構包含一些程序的資訊。具體細節可以檢視msdn.
緊接着初始化堆 _heap_init(1)
初始化堆是很重要的,否則不能使用C++的new 或c的malloc來配置設定記憶體。
然後初始化多線程 _mtinit()
然後初始化I/O,_ioinit(),得到指令行參數GetCommandLineT();得到目前程序程序版本資訊
最後調用啟動函數
WinMain((HINSTANCE) & __ImageBase,NULL,lpszCommandLine,StartupInfo.dwFlags & STARTF_USESHOWWINDOW? StartupInfo.wShowWindow: SW_SHOWDEFAULT);
到這裡就可以看見,winMain的參數是怎麼來的了,hInsteance 就是__ImageBase(載入基址),指令行參數也是傳進來的,最後的iCmdShow,參數就是STRTUPINFO裡的顯示方式。
就是因為編譯時加入了啟動函數是以使C程式編譯出來的可執行檔案比彙程式設計的大了30多K。
其實啟動函數不是必須的,可以自定義一個自己的啟動函數代替預設的啟動函數。
比如定義一個
int WINAPI main()
{
HINSTANCE hInstance = GetModuleHandle(NULL); //得到目前程序的句柄,和彙編一樣
LPSTR lpszCmdLine = GetCommandLine(); //獲得指令行參數
int r = WinMain(hInstance, NULL, lpszCmdLine, SW_SHOWDEFAULT); //調用WinMain函數,就開始執行
ExitProcess(r); //最後結束程序
return r; // this will never be reached.
}
需要在link.exe 後加/entry:main /nodefaultlib:msvcrt90.lib參數,/entry指定入口點函數, /nodefaultlib指定不連接配接運作時庫。
這樣編譯連接配接後,可執行檔案尺寸和彙編後的大小一樣。反彙編後比較内容也基本差不多。要不說C語言執行速度快,編譯後的内容和直接用彙編寫的程式基本上一樣。
windows 下32 位彙 編語 言學 習筆 記 第十章 記憶體管理部分
前面5 6 7 8 9章都是介紹windows 界面設計有關的内容,這些内容大概看一下就可以,等需要用的時候再回過頭來查。一次性記 住的可能性不大。這些章節也沒有什麼難度,自己看看就可以。
我所關心的還是windows 系統相關知識,說道界面設定,對RC資源再熟悉做界面還是Delphi,C++builder比較快速。
本章介紹了很多windows 下關于記憶體管理的函數,書上有句話說的很好,不要去深究這些函數配置設定的記憶體具體放在記憶體尋址空間的什麼地方,隻需要知道什麼時候該用什麼類型的記憶體管理函數就可以。
Global标準記憶體管理
GlobalAlloc函數使用GMEM_MOVEABLE參數傳回的是個記憶體句柄,内容是一個位址,這個位址才是可以使用的記憶體塊。比如:
PVOID lpMem = GlobalAlloc(GHND, 1000);
現在lpMem指向的内容是一個位址,必須通過GlobalLock(lpMem)函數傳回這個指針才能使用。
實際上GMEM_MOVEABLE參數和GMEM_FIXED差別就是前者必須要經過GlobalLock才能使用,後者則通過傳回的指針直接使用。
關于什麼時候可移動,我沒法測,不知道windows 在什麼情況下去移動這塊記憶體。
GlobalAlloc配置設定記憶體是緊湊的,就是說配置設定的多個記憶體空間可以放在一個記憶體頁面裡,隻要能放的下。
Heap堆記憶體管理
堆就是程序位址空間的總和,一個程序預設隻有一個堆,用GlobalAlloc會在這個預設的堆中配置設定記憶體,如想用堆管理函數必須首先自己建立一個堆,然後在這個堆中申請記憶體。
這又有什麼用呢?如果寫一個程式,有20個線程,每個線程都在不停進行着配置設定記憶體和釋放記憶體操作,過不多久,就會發現“記憶體内被弄得亂七八糟“,甚至會出現很奇觀的錯誤。為了避免此類問題發生,可以建立幾個堆,讓部分線程在其他的堆中申請記憶體操作。
想使用堆,必須用HeapCreate來建立它,這個堆就是私有的了。注意堆建立後要釋放的,用HeapDestroy釋放。若想使用自己建立的私有的堆,為線程在私有堆配置設定記憶體,可以HeapAlloc完成,這個函數中有上次建立的私有堆的句柄,标志,大小,用法非常簡單,用完記憶體要釋放,用HeapFree完成,需要指定堆的句柄,标志,記憶體塊的位址,用法同樣很簡單。
還有一些不常用的堆管理函數,如GetProcessHeap,用來取預設堆句柄,程式一開始時的那個堆,GetProcessHeaps則是列舉一個程序中所有的堆,HeapWalk用來列舉一個堆中所有的記憶體塊,HeapValidate用來驗證一個堆的記憶體塊的有效性。
虛拟記憶體管理
有一點必須知道,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函數在不同程序中的位址是一樣的,可以直接再目标程序使用。
比如下面的例子:
// 向目标程序位址空間寫入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裡的還多了可以知道哪裡的記憶體是程序堆,那裡映射,映像了什麼檔案。
部分參考windows 核心程式設計第10章的内容
代碼:
;顯示記憶體資訊
.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定義一個字元串。
依次顯示:Address,State,Type,RegionSize,Protect,其他描述
彙編 的串指令
彙編 中有一些專門用來處理連續記憶體單元的指令,叫做串操作指令。串指令通過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進行增減。
本章的内容需要熟練掌握,不管寫什麼樣的程式,對可用記憶體的利用的和了解都是很關鍵的。對于本章,我計劃多寫一些小程式,1來加深對内從使用的了解,2來熟悉彙編 文法。
windows 下32 位彙 編語 言學 習筆 記 第十章 記憶體管理部分 2
這兩天通過寫彙編 程式,越來越發現彙編 很有意思。自己規劃每一個寄存器的使用,設計每一個跳轉和分支,這不同于使用其他進階語言 ,所有資源對于程式設計者都是透明的,讓我有一種盡在掌握的感覺,而且每寫一個程式都很有成就感,這是我用别的語言 寫程式所沒有的感覺。
不管學習 什麼東西,實踐是最重要的,計算機程式設計這種實踐性很強的科目更是如此。有的東西看似簡單,實際動起手來可就不那麼容易了,是以必須要告誡大家,學習 計算機程式設計,必須要勤動手,不能懶惰。如果你能夠把 windows 程式設計,windows 核心程式設計,windows 32 位彙 編語 言,這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++寫的循環,判斷)看看編譯器是如何組織的。
Alloc Num用來輸入需要保留多少個頁面檔案。保留後,使用use送出在Index後面Edit文本框輸入的值(0<=值< Alloc Num)的這塊記憶體頁。Clear用來釋放指定值的記憶體頁,Clear All釋放所有送出的記憶體。下面的Memory View檢視配置設定内 存的頁面資訊。每次送出,清除後都會重新整理顯示。