您的位置:首页 > 其它

程序的加载和执行(三)——《x86汇编语言:从实模式到保护模式》读书笔记23

2016-03-26 18:28 369 查看

程序的加载和执行(三)——读书笔记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.符号表的重定位

为了使用内核提供的例程,用户程序需要建立一个符号表。当用户程序被加载后,内核会根据这个符号表来回填每个例程的入口地址。这个过程就是符号地址的重定位。重定位过程中必不可少的环节是字符串的比较和匹配。

为了对用户程序的符号表进行匹配,内核也必须建立一张符号表,这张符号表包含了内核提供的所有例程。

329;===============================================================================
330     SECTION core_data vstart=0             ;系统核心的数据段
331;-------------------------------------------------------------------------------
332         pgdt             dw  0             ;用于设置和修改GDT
333                          dd  0
334
335         ram_alloc        dd  0x00100000    ;下次分配内存时的起始地址
336
337         ;符号地址检索表
338         salt:
339         salt_1           db  '@PrintString'
340                     times 256-($-salt_1) db 0
341                          dd  put_string
342                          dw  sys_routine_seg_sel
343
344         salt_2           db  '@ReadDiskData'
345                     times 256-($-salt_2) db 0
346                          dd  read_hard_disk_0
347                          dw  sys_routine_seg_sel
348
349         salt_3           db  '@PrintDwordAsHexString'
350                     times 256-($-salt_3) db 0
351                          dd  put_hex_dword
352                          dw  sys_routine_seg_sel
353
354         salt_4           db  '@TerminateProgram'
355                     times 256-($-salt_4) db 0
356                          dd  return_point
357                          dw  core_code_seg_sel
358
359         salt_item_len   equ $-salt_4
360         salt_items      equ ($-salt)/salt_item_len


以上代码中第339~360,就是内核的符号表。

我们再看一下用户程序中定义的用户符号表(在文件c13.asm中)。

24;-------------------------------------------------------------------------------
25         ;符号地址检索表
26         salt_items       dd (header_end-salt)/256 ;#0x24
27
28         salt:                                     ;#0x28
29         PrintString      db  '@PrintString'
30                     times 256-($-PrintString) db 0
31
32         TerminateProgram db  '@TerminateProgram'
33                     times 256-($-TerminateProgram) db 0
34
35         ReadDiskData     db  '@ReadDiskData'
36                     times 256-($-ReadDiskData) db 0


内核符号表的每个条目包括两部分:

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 edi

512  .b5:   pop edi        ;.b5这个标号是我自己加的,后面会讲到
513         add edi,256    ;指向用户符号表的下一个条目
514         pop ecx
515         loop .b2


487~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 .b3


490~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行应该插入一段代码:

pop ecx
pop esi
pop edi
jmp .b5 ;跳转到512行


其实这几行代码中,寄存器
ECX
,
ESI
,
EDI
里面的值是不重要的。

因为在514行,
ECX
会获得合适的值;

在512~513行,
EDI
会获得合适的值;

在491行,
ESI
会获得合适的值;

所以上面的补丁可以修改为:

add esp,12    ;使栈平衡
jmp .b5       ;跳转到512行


这样就简洁多了。

可能有的读者不太相信,觉得配书源码不应该有问题,是不是我搞错了。这没有关系,我会在后面的博文中证明这确实是一个BUG。“实践出真知。”

好了,这篇博文就说到这里。下次我们讲用户程序的执行。

【end】
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: