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

程序的加载和执行(三)——读书笔记23

接着上次的内容说。

关于过程load_relocate_program的讲解还没有完,还差创建栈段描述符和重定位符号表。

分配栈空间与创建栈段描述符

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,计算出描述符中的段界限,计算公式是:

如果不明白为什么是这个公式,可以参考我的博文:《如何构造栈段描述符》

http://blog.csdn.net/longintchar/article/details/50967180

466~469,调用过程allocate_memory申请栈空间;

470:准备参数EAX,因为描述符中的基地址等于栈空间的低端物理地址加上栈的大小。不懂的还请参考我上面提到的博文。

472~473,创建并安装栈段描述符。

474:将选择子回填到对应的位置(请参考上图)。

符号表的重定位

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

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

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个字节被重新填写,填写的是对应例程的入口。

上面的过程可以用一张图来说明:

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,就是在匹配之后,没有跳出到外循环,而是和内核符号表的下一个条目再次比较了。后文会仔细分析这个问题。

外循环的代码

先来看看外循环:

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:因为内循环也要用到ECXEDI,所以进入内循环前先把它们压栈保存;

513:EDI加上256,于是指向上图中U-SALT表格的下一个条目;

对于外循环ES:EDI指向的这个条目,在内循环中要把它和内核符号表的所有条目进行比较(最坏的情况)。

内循环的代码

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的值,使其指向内核符号表的下一个条目。

对比的核心代码

我们再看一下对比的核心代码:

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:ESIES: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】

时间: 2024-10-15 23:34:00

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

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

程序的加载和执行(六)--<x86汇编语言:从实模式到保护模式>读书笔记26 通过本文能学到什么? NASM的条件汇编 用NASM编译的时候,通过命令行选项定义宏 Makefile的条件语句 在make命令行中覆盖Makefile中的变量值 第13章习题解答 复习如何构造栈段描述符 我们接着上篇博文说. 在我修改后的文件中,用到了条件汇编. 比如: %ifdef DEBUG put_core_salt: ;打印内核的符号 ... ... put_usr_salt: ;打印用户的符号 ... .

JQuery的几种页面加载完执行三种方式

jquery加载页面的方法(页面加载完成就执行) 1. 1 $(function(){ 2 $("#a").click(function(){ 3 //adding your code here 4 }); 5 }); 2. 1 $(document).ready(function(){ 2 $("#a").click(function(){ 3 //adding your code here 4 }); 5 }); 3. 1 window.onload = fun

Linux0.11内核--加载可执行二进制文件之3.exec

最后剩下最核心的函数do_execve了,由于这里为了简单起见我不分析shell命令的情况, /* * 'do_execve()'函数执行一个新程序. */ //// execve()系统中断调用函数.加载并执行子进程(其它程序). // 该函数系统中断调用(int 0x80)功能号__NR_execve 调用的函数. // 参数:eip - 指向堆栈中调用系统中断的程序代码指针eip 处,参见kernel/system_call.s 程序 // 开始部分的说明:tmp - 系统中断调用本函数时

[转]JavaScript 的性能优化:加载和执行

原文链接:http://www.ibm.com/developerworks/cn/web/1308_caiys_jsload/index.html?ca=drs- JavaScript 的性能优化:加载和执行 蔡 愉晟, 软件工程师, IBM 简介: 随着 Web2.0 技术的不断推广,越来越多的应用使用 JavaScript 技术在客户端进行处理,从而使 JavaScript 在浏览器中的性能成为开发者所面临的最重要的可用性问题.而这个问题又因 JavaScript 的阻塞特性变的复杂,也就

【转】js JavaScript 的性能优化:加载和执行

JavaScript 的性能优化:加载和执行 转自:https://www.ibm.com/developerworks/cn/web/1308_caiys_jsload/ 随着 Web2.0 技术的不断推广,越来越多的应用使用 JavaScript 技术在客户端进行处理,从而使 JavaScript 在浏览器中的性能成为开发者所面临的最重要的可用性问题.而这个问题又因 JavaScript 的阻塞特性变的复杂,也就是说当浏览器在执行 JavaScript 代码时,不能同时做其他任何事情.本文详

jvm内存模型,java类从编译到加载到执行的过程,jvm内存分配过程

一.jvm内存模型 JVM 内存模型主要分为堆.程序计数器.方法区.虚拟机栈和本地方法栈 1.堆 1.1.堆是 JVM 内存中最大的一块内存空间. 1.2.该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中. 1.3.堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成. 2.程序计数器(Program Counter Register) 程序计数器是一块很小的内存

JavaScript 的性能优化:加载和执行

随着 Web2.0 技术的不断推广,越来越多的应用使用 JavaScript 技术在客户端进行处理,从而使 JavaScript 在浏览器中的性能成为开发者所面临的最重要的可用性问题.而这个问题又因 JavaScript 的阻塞特性变的复杂,也就是说当浏览器在执行 JavaScript 代码时,不能同时做其他任何事情.本文详细介绍了如何正确的加载和执行 JavaScript 代码,从而提高其在浏览器中的性能. 概览 无论当前 JavaScript 代码是内嵌还是在外链文件中,页面的下载和渲染都必

怎么样加快JavaScript加载和执行效率

概览 无论当前 JavaScript 代码是内嵌还是在外链文件中,页面的下载和渲染都必须停下来等待脚本执行完成.JavaScript 执行过程耗时越久,浏览器等待响应用户输入的时间就越长.浏览器在下载和执行脚本时出现阻塞的原因在于,脚本可能会改变页面或 JavaScript 的命名空间,它们对后面页面内容造成影响.一个典型的例子就是在页面中使用document.write(). JavaScript 代码内嵌示例 <html> <head> <title>Source

浏览器环境下JavaScript脚本加载与执行探析之动态脚本与Ajax脚本注入

在<浏览器环境下JavaScript脚本加载与执行探析之defer与async特性>中,我们研究了延迟脚本(defer)和异步脚本(async)的执行时机.浏览器支持情况.浏览器bug以及其他的细节问题.而除了defer和async特性,动态脚本和Ajax脚本注入也是两种常用的创建无阻塞脚本的方法.总的来看,这两种方法都能达到脚本加载不影响页面解析和渲染的作用,但是在不同的浏览器中,这两种技术所创建的脚本的执行时机还是有一定差异,今天我们再来探讨一下通过动态脚本技术和Ajax注入的脚本在这些方