程序的加载和执行(三)——《x86汇编语言:从实模式到保护模式》读书笔记23
程序的加載和執(zhí)行(三)——讀書筆記23
接著上次的內(nèi)容說(shuō)。
關(guān)于過(guò)程load_relocate_program的講解還沒(méi)有完,還差創(chuàng)建棧段描述符和重定位符號(hào)表。
1.分配棧空間與創(chuàng)建棧段描述符
462 ;建立程序堆棧段描述符 463 mov ecx,[edi+0x0c] ;4KB的倍率 464 mov ebx,0x000fffff 465 sub ebx,ecx ;得到段界限 466 mov eax,4096 467 mul dword [edi+0x0c] 468 mov ecx,eax ;準(zhǔn)備為堆棧分配內(nèi)存 469 call sys_routine_seg_sel:allocate_memory 470 add eax,ecx ;得到堆棧的高端物理地址 471 mov ecx,0x00c09600 ;4KB粒度的堆棧段描述符 472 call sys_routine_seg_sel:make_seg_descriptor 473 call sys_routine_seg_sel:set_up_gdt_descriptor 474 mov [edi+0x08],cx說(shuō)代碼之前,先上圖,用戶程序的頭部示意圖:
提醒一下,這時(shí)候DS:EDI依然指向用戶程序的起始位置。
463行,取得用戶設(shè)置的棧段的大小(以4KB為單位),就是下面公式中的N;
464~465,計(jì)算出描述符中的段界限,計(jì)算公式是:
如果不明白為什么是這個(gè)公式,可以參考我的博文:
《如何構(gòu)造棧段描述符》
466~469,調(diào)用過(guò)程allocate_memory申請(qǐng)棧空間;
470:準(zhǔn)備參數(shù)EAX,因?yàn)槊枋龇械幕刂返扔跅?臻g的低端物理地址加上棧的大小。不懂的還請(qǐng)參考我上面提到的博文。
472~473,創(chuàng)建并安裝棧段描述符。
474:將選擇子回填到對(duì)應(yīng)的位置(請(qǐng)參考上圖)。
2.符號(hào)表的重定位
為了使用內(nèi)核提供的例程,用戶程序需要建立一個(gè)符號(hào)表。當(dāng)用戶程序被加載后,內(nèi)核會(huì)根據(jù)這個(gè)符號(hào)表來(lái)回填每個(gè)例程的入口地址。這個(gè)過(guò)程就是符號(hào)地址的重定位。重定位過(guò)程中必不可少的環(huán)節(jié)是字符串的比較和匹配。
為了對(duì)用戶程序的符號(hào)表進(jìn)行匹配,內(nèi)核也必須建立一張符號(hào)表,這張符號(hào)表包含了內(nèi)核提供的所有例程。
以上代碼中第339~360,就是內(nèi)核的符號(hào)表。
我們?cè)倏匆幌掠脩舫绦蛑卸x的用戶符號(hào)表(在文件c13.asm中)。
內(nèi)核符號(hào)表的每個(gè)條目包括兩部分:
1. 256字節(jié)的符號(hào)名,不足的部分用零填充;
2. 例程的入口(4字節(jié)的偏移地址+2字節(jié)的段選擇子);
用戶符號(hào)表的每個(gè)條目只有一個(gè)部分:
256字節(jié)的符號(hào)名,不足的部分用零填充。
當(dāng)內(nèi)核對(duì)用戶符號(hào)表完成重定位后,用戶符號(hào)表的內(nèi)容發(fā)生了改變:每個(gè)條目的前6個(gè)字節(jié)被重新填寫,填寫的是對(duì)應(yīng)例程的入口。
上面的過(guò)程可以用一張圖來(lái)說(shuō)明:
2.1.CMPS指令
在講述代碼之前,我們先學(xué)習(xí)字符串比較指令cmps。該指令有3種形式,分別用于字節(jié)、字和雙字的比較。
cmpsb ;字節(jié)比較cmpsw ;字比較cmpsd ;雙字比較在16位模式下,源字符串的首地址由DS:SI指定,目的字符串的首地址由ES:DI指定;
在32位模式下,源字符串的首地址由DS:ESI指定,目的字符串的首地址由ES:EDI指定;
在處理器內(nèi)部,cmps指令的操作是把兩個(gè)操作數(shù)相減,然后根據(jù)結(jié)果設(shè)置相應(yīng)的標(biāo)志位。這還沒(méi)有完,還要根據(jù)DF的值調(diào)整(E)SI和(E)DI的值。下圖是從《Intel Architecture Software Developer’s Manual Volume 2:Instruction Set Reference》弄過(guò)來(lái)的,用偽代碼描述了操作過(guò)程。
REP/REPE/REPZ/REPNE/REPNZ指令
單純的cmps指令只比較一次,如果要連續(xù)比較,需要加指令前綴rep;連續(xù)比較的次數(shù)由CX(16位模式下)或者ECX(32位模式下)控制。除了rep前綴,還有repe(repz),表示相等則重復(fù);repne(repnz)表示不相等則重復(fù)。用這些前綴結(jié)合cmps比較時(shí),操作過(guò)程如下:
由此可見,repe(repz)用于搜索第一個(gè)不相等的字節(jié)、字或者雙字,repne(repnz)用來(lái)搜索第一個(gè)相等的字節(jié)、字或者雙字。
好了,有了以上鋪墊,我們可以進(jìn)入代碼的學(xué)習(xí)了。
476 ;重定位SALT 477 mov eax,[edi+0x04] 478 mov es,eax ;es -> 用戶程序頭部 479 mov eax,core_data_seg_sel 480 mov ds,eax 481 482 cld 483 484 mov ecx,[es:0x24] ;用戶程序的SALT條目數(shù) 485 mov edi,0x28 ;用戶程序內(nèi)的SALT位于頭部?jī)?nèi)0x28處477~478:把之前安裝好的頭部段選擇子賦值給ES;(注意,DS依然指向0-4GB內(nèi)存段,EDI中的值是程序加載的物理地址,所以[edi+0x04]就可以尋址到頭部段的選擇子。)
479~480:DS指向核心數(shù)據(jù)段;
482:令DF標(biāo)志位=0,采用正向比較;
484:如下圖所示,把用戶的符號(hào)表的條目數(shù)傳入ECX;
485:令ES:EDI指向第一個(gè)符號(hào)。
為了說(shuō)明代碼思路,還是引用書上的一張圖吧:
思路是兩層循環(huán),分為外循環(huán)和內(nèi)循環(huán)。外循環(huán)的作用是從用戶符號(hào)表依次取出符號(hào)1,符號(hào)2,…符號(hào)N;內(nèi)循環(huán)的作用是遍歷內(nèi)核符號(hào)表的每一個(gè)條目,同外循環(huán)取出的那個(gè)條目進(jìn)行對(duì)比。如果匹配,則復(fù)制偏移地址和段選擇子,之后跳出到外循環(huán)。
請(qǐng)注意紅色的字。配書代碼有一個(gè)小小的BUG,就是在匹配之后,沒(méi)有跳出到外循環(huán),而是和內(nèi)核符號(hào)表的下一個(gè)條目再次比較了。后文會(huì)仔細(xì)分析這個(gè)問(wèn)題。
2.2.外循環(huán)的代碼
先來(lái)看看外循環(huán):
486 .b2: 487 push ecx ;初始值為用戶程序的符號(hào)數(shù)目,每次外循環(huán)都減一 488 push edi512 .b5: pop edi ;.b5這個(gè)標(biāo)號(hào)是我自己加的,后面會(huì)講到 513 add edi,256 ;指向用戶符號(hào)表的下一個(gè)條目 514 pop ecx 515 loop .b2487~488:因?yàn)閮?nèi)循環(huán)也要用到ECX和EDI,所以進(jìn)入內(nèi)循環(huán)前先把它們壓棧保存;
513:EDI加上256,于是指向上圖中U-SALT表格的下一個(gè)條目;
對(duì)于外循環(huán)ES:EDI指向的這個(gè)條目,在內(nèi)循環(huán)中要把它和內(nèi)核符號(hào)表的所有條目進(jìn)行比較(最壞的情況)。
2.3.內(nèi)循環(huán)的代碼
490 mov ecx,salt_items ;內(nèi)核符號(hào)總數(shù)目 491 mov esi,salt ;指向內(nèi)核的第一個(gè)符號(hào) 492 .b3: 493 push edi 494 push esi 495 push ecx;這里放置實(shí)際進(jìn)行對(duì)比的代碼506 pop ecx 507 pop esi 508 add esi,salt_item_len ;指向內(nèi)核符號(hào)表的下一個(gè)條目 509 pop edi 510 loop .b3490~491:每次從外循環(huán)進(jìn)入內(nèi)循環(huán)的時(shí)候,都要初始化內(nèi)循環(huán)的對(duì)比次數(shù)(=內(nèi)核符號(hào)總數(shù)目),并且重新讓ESI指向內(nèi)核符號(hào)表(C-SALT)的起始。這相當(dāng)于內(nèi)循環(huán)的初始化,可以想象成C語(yǔ)言中for語(yǔ)句
for(ecx = salt_items,esi = salt; ...; ...)493~495:因?yàn)樵趯?shí)際對(duì)比的時(shí)候,會(huì)改變ESI,EDI,ECX的值,所以要在實(shí)際對(duì)比之前把這些寄存器壓棧保存。
506~509:恢復(fù)上述壓棧的寄存器,并且增加ESI的值,使其指向內(nèi)核符號(hào)表的下一個(gè)條目。
2.4.對(duì)比的核心代碼
我們?cè)倏匆幌聦?duì)比的核心代碼:
497 mov ecx,64 ;檢索表中,每條目的比較次數(shù) 498 repe cmpsd ;每次比較4字節(jié) 499 jnz .b4 ;ZF=0表示不匹配,則跳轉(zhuǎn) 500 mov eax,[esi] ;若匹配,esi恰好指向其后的地址數(shù)據(jù) 501 mov [es:edi-256],eax ;將字符串改寫成偏移地址 502 mov ax,[esi+4] 503 mov [es:edi-252],ax ;以及段選擇子 504 .b4: 505每當(dāng)執(zhí)行到這里,DS:ESI和ES:EDI都分別指向內(nèi)核符號(hào)表和用戶符號(hào)表中的某個(gè)條目。
497:因?yàn)橐粋€(gè)符號(hào)占用256字節(jié),我們用的是cmpsd指令,所以最多需要比較256/4=64次,于是向ECX傳入64;
498:如果相等就繼續(xù)比較;停止條件是(ECX==0) || (ZF==0),也就是ECX為0或者發(fā)現(xiàn)了不相等就停止比較。
499:假如比較發(fā)現(xiàn)了不相等,于是ZF=0;假如字符串是相等的,那么會(huì)重復(fù)比較64次,最后ZF=1;所以ZF=0說(shuō)明不匹配,反之匹配。
如果不匹配,就跳轉(zhuǎn)到.b4標(biāo)號(hào)處。其實(shí)就是跳到內(nèi)循環(huán)的506行。
506:恢復(fù)ECX的值,這個(gè)值表示還剩多少次內(nèi)循環(huán)(對(duì)于某個(gè)用戶符號(hào),還剩多少個(gè)內(nèi)核符號(hào)要和它比較);
509:恢復(fù)EDI的值,也就是讓EDI再次指向當(dāng)前用戶符號(hào)的起始。
500~501:如果匹配,那么這時(shí)候ESI剛好指向了內(nèi)核某匹配上的符號(hào)(總共256字節(jié))的末尾,后面就是4字節(jié)的偏移地址和2字節(jié)的段選擇子。將偏移地址回填到某用戶符號(hào)的開始處;
502~503:將段選擇子回填到偏移地址的后面,于是這個(gè)段選擇子就和前面的偏移地址組成了例程的入口。到時(shí)候用戶程序就能利用這個(gè)入口,來(lái)個(gè)華麗的遠(yuǎn)調(diào)用或者遠(yuǎn)跳轉(zhuǎn)。
這個(gè)代碼說(shuō)到這里就結(jié)束了嗎?No,No.前文提到過(guò),這里是有個(gè)小問(wèn)題的。在500~503執(zhí)行完后,應(yīng)該怎么辦?既然匹配成功了,該填的也填了,那么就應(yīng)該讓EDI指向下一個(gè)符號(hào),讓ESI指向內(nèi)核符號(hào)表的起始,也就是說(shuō)跳出內(nèi)循環(huán),進(jìn)入下一輪外循環(huán)(跳到512行開始執(zhí)行,相當(dāng)于C語(yǔ)言中的break)。但是還牽扯到一個(gè)問(wèn)題,在跳轉(zhuǎn)到512行之前,我們應(yīng)該使棧平衡。因?yàn)樵?93~495壓入了三個(gè)寄存器,然后進(jìn)行實(shí)際的比較,比較之后,也應(yīng)該彈出這三個(gè)寄存器。
所以505行應(yīng)該插入一段代碼:
其實(shí)這幾行代碼中,寄存器ECX,ESI,EDI里面的值是不重要的。
因?yàn)樵?14行,ECX會(huì)獲得合適的值;
在512~513行,EDI會(huì)獲得合適的值;
在491行,ESI會(huì)獲得合適的值;
所以上面的補(bǔ)丁可以修改為:
這樣就簡(jiǎn)潔多了。
可能有的讀者不太相信,覺(jué)得配書源碼不應(yīng)該有問(wèn)題,是不是我搞錯(cuò)了。這沒(méi)有關(guān)系,我會(huì)在后面的博文中證明這確實(shí)是一個(gè)BUG。“實(shí)踐出真知。”
好了,這篇博文就說(shuō)到這里。下次我們講用戶程序的執(zhí)行。
【end】
總結(jié)
以上是生活随笔為你收集整理的程序的加载和执行(三)——《x86汇编语言:从实模式到保护模式》读书笔记23的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Markdown编辑器攻略——字体,字号
- 下一篇: 爬虫项目之豆瓣电影排行榜前10页