程序的加载和执行(三)——《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
说代码之前,先上图,用户程序的头部示意图:
![](http://i.imgur.com/ThMJGKt.png)
提醒一下,这时候DS:EDI依然指向用户程序的起始位置。
463行,取得用户设置的栈段的大小(以4KB为单位),就是下面公式中的
N;
464~465,计算出描述符中的段界限,计算公式是:
![](http://i.imgur.com/belpTMD.png)
如果不明白为什么是这个公式,可以参考我的博文:
《如何构造栈段描述符》
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个字节被重新填写,填写的是对应例程的入口。
上面的过程可以用一张图来说明:
![](http://i.imgur.com/p7EPpRX.png)
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》弄过来的,用伪代码描述了操作过程。
![](http://i.imgur.com/McTxVX0.png)
REP/REPE/REPZ/REPNE/REPNZ指令
单纯的cmps指令只比较一次,如果要连续比较,需要加指令前缀rep;连续比较的次数由
CX(16位模式下)或者
ECX(32位模式下)控制。除了
rep前缀,还有
repe(repz),表示相等则重复;
repne(repnz)表示不相等则重复。用这些前缀结合cmps比较时,操作过程如下:
![](http://i.imgur.com/W5u9wKr.png)
由此可见,
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指向第一个符号。
为了说明代码思路,还是引用书上的一张图吧:
![](http://i.imgur.com/miUQd5t.png)
思路是两层循环,分为外循环和内循环。外循环的作用是从用户符号表依次取出符号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】
相关文章推荐
- [Libav-user] aac encoder in real time scenario
- Eclipse连接hadoop集群配置,亲测有效!
- hdu1244 Max Sum Plus Plus Plus--DP
- 共勉属于我们的移动互联网时代: Keep Moving Forward ~!
- Javascript初学好玩
- Visual Studio中自动添加自定义消息映射
- ZM809是一款全球最小的3G安卓智能模块
- XFire创建WebService实例应用
- 配置JAVA环境变量
- Android Studio 配置 gradle 脚本错误:Gradle DSL method not found: 'minus()
- Linux中的用户丶组管理
- Centeros环境中Tomcat配置域名
- Java--json序列化与反序列化
- 怎么让cmd命令行不关闭
- JavaScript编程全解学习日志 二
- 安卓 jni 开发之 native 方法的动态注册
- [面试] 算法(二)—— 第一个出现一次的字符(第一个出现 k 次、出现最多次)
- JAVA遍历指定文件夹路径,返回全部JPG图片路径
- c++ 辗转相除法 递归非递归
- laravel框架学习前准备