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