《程序员自我修养》第十章读书笔记

来源:互联网 发布:电脑下载软件哪个好 编辑:程序博客网 时间:2024/06/06 05:31

第十章主要对程序的运行时内存布局进行分析。而本书接下来的几章主要是针对程序的运行环境进行研究。

首先来看程序的内存布局

虽然当前的内存空间使用平坦模型,即整个内存是一个统一的地址空间,用户可以使用一个32位的指针访问任意内存位置。但操作系统对于却不是将所有资源都交给用户的应用程序使用。在linux下默认将高地址1GB的空间分配给内核。

一般来讲,应用程序使用的内存空间里有如下“默认”的区域:

栈:用于维护函数调用的上下文(包括main函数),我个人感觉栈就是用于保存程序运行时所需要的参数、信息等。

堆:堆是用来容纳应用程序动态分配的内存区域,当程序使用malloc或new分配内存时,得到的内存来自堆里。

可执行文件映像:这里存储着可执行文件在内存里的映像,由装载器在装载时将可执行文件的内存读取或映射到这里。

保留区:保留区并不是一个单一的内存区域,而是对内存中收到保护而禁止访问的内存区域的总称,例如,大多数操作系统里,极小的地址通常都是不允许访问的,如NULL。这个区域其实在一定程度上起到了对进程地址空间的保护作用。对于使用空指针或小整型值指针引用内存的情况,操作系统可以马上就进行阻止,并产生“段错误”异常。

动态链接器映射区:这个区域用于映射装载的动态链接库。在linux下,如果可执行文件依赖于其他共享库,那么系统就会为它在从0x40000000(32位操作系统)开始的地址分配相应的空间,并将共享库装入该空间。动态链接器也是加载到这一地址,而后开始自举代码的功能。

在这里还是要给大家分享一篇内容上比较全面的blog:http://www.cnblogs.com/clover-toeic/p/3754433.html

通过对上述几个区域的分析,马上就可以与我们学到的知识联系到一起,这里每一区域就对应着我们在第七章中看到的进程的内存分布。在这里还是给大家先看一个进程的内存分布:

cat /proc/5735/maps00400000-00401000 r-xp 00000000 08:08 1187187                            /home/andywang/project/DSO/program100600000-00601000 r--p 00000000 08:08 1187187                            /home/andywang/project/DSO/program100601000-00602000 rw-p 00001000 08:08 1187187                            /home/andywang/project/DSO/program17facdc69a000-7facdc85a000 r-xp 00000000 08:08 3412306                    /lib/x86_64-linux-gnu/libc-2.21.so7facdc85a000-7facdca5a000 ---p 001c0000 08:08 3412306                    /lib/x86_64-linux-gnu/libc-2.21.so7facdca5a000-7facdca5e000 r--p 001c0000 08:08 3412306                    /lib/x86_64-linux-gnu/libc-2.21.so7facdca5e000-7facdca60000 rw-p 001c4000 08:08 3412306                    /lib/x86_64-linux-gnu/libc-2.21.so7facdca60000-7facdca64000 rw-p 00000000 00:00 0 7facdca64000-7facdca65000 r-xp 00000000 08:08 1187174                    /home/andywang/project/DSO/libtest.so7facdca65000-7facdcc64000 ---p 00001000 08:08 1187174                    /home/andywang/project/DSO/libtest.so7facdcc64000-7facdcc65000 r--p 00000000 08:08 1187174                    /home/andywang/project/DSO/libtest.so7facdcc65000-7facdcc66000 rw-p 00001000 08:08 1187174                    /home/andywang/project/DSO/libtest.so7facdcc66000-7facdcc8a000 r-xp 00000000 08:08 3412278                    /lib/x86_64-linux-gnu/ld-2.21.so7facdce69000-7facdce6c000 rw-p 00000000 00:00 0 7facdce86000-7facdce89000 rw-p 00000000 00:00 0 7facdce89000-7facdce8a000 r--p 00023000 08:08 3412278                    /lib/x86_64-linux-gnu/ld-2.21.so7facdce8a000-7facdce8b000 rw-p 00024000 08:08 3412278                    /lib/x86_64-linux-gnu/ld-2.21.so7facdce8b000-7facdce8c000 rw-p 00000000 00:00 0 7ffdc783c000-7ffdc785d000 rw-p 00000000 00:00 0                          [stack]7ffdc794e000-7ffdc7950000 r--p 00000000 00:00 0                          [vvar]7ffdc7950000-7ffdc7952000 r-xp 00000000 00:00 0                          [vdso]ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

通过上面的图可以发现:

  1. “栈”区对应着[stack]。
  2. 由于程序中没有使用malloc等函数,因此不存在[heap]。
  3. “可执行文件映像”区一共对应三个VMA,通过对这三个VMA的权限进行分析,这三个VMA应该属于三个不同的“节”。通过readelf -l命令查看程序,发现其中load属性的节仅有两个,这两个节的标志分别是“RE”与“RW”,与第一个和第三个VMA对上了,但第二个“R”权限的VMA还没有对应的节。据本人估计应该是GNU_RELRO节,因为这个节的虚拟地址与权限都符合,在晚上找了找,没找到有关于这个节的信息,欢迎了解的同学给我补充。
  4. 动态链接器映射区中共包括三个不同的动态链接库,分别是glibc与ld,以及自己编写的libtest.so。而这三个动态链接库又分别对应这三个不同的VMA。
  5. 有三个比较特殊的VMA,分别是vvar、vdso、vsyscall,今天咱们先专注于书中的内容,这些内容留待以后在分析。

关于linux如何在可执行程序与进程的虚拟空间之间建立联系的,请见下面这篇文章:http://blog.chinaunix.net/uid-26833883-id-3193585.html

10.2 主要对栈进行了分析。有关于栈的基础知识在此就不给大家分享了,总结起来就是一句话“先进后出”。栈对于程序运行的作用主要在于栈“保存了一个函数调用所需要的维护信息,以上内容就是堆栈帧(stack frame)或活动记录(activate record)。堆栈帧主要由以下几部分内容组成:

  1. 函数的返回地址与参数
  2. 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
  3. 保存的上下文:包括在函数调用前后需要保持不变的寄存器。

在i386中,esp始终指向栈的顶部,同时也就指向了当前函数活动记录的顶部。而相对的,edp 指向了函数活动记录的一个固定位置(基本就可以是顶部),ebp 寄存器又被称为帧指针(frame pointer)。这里要明确的一个概念是:某个函数的活动记录是指,从函数参数开始到esp寄存器所指的部分,ebp 虽然不直接指向这一位置,但之所以认为ebp是函数活动记录的底部,是由于之前的内容写入栈中后就不会在改变(与临时变量的分配相对应)。ebp 所直接指向的数据是调用该函数前ebp的值,这样在函数返回的时候,ebp 可以通过读取这个值恢复到调用前的值,即通过这一操作可以实现函数返回时函数活动记录的快速清除。

接下来让我们结合实例来看看,函数活动记录是如何形成这种形式的:

先来看看理论上的东西,在i386(确切的说是stdcall调用惯例)下的函数调用方式如下:

  1. 参数入栈(不过在x86-64下,函数通过不同的寄存器进行传递)
  2. 把当前指令的下一条指令的地址(返回地址)压入栈中,此时函数活动记录的雏形已经形成,只差ebp的入栈了。
  3. 跳转到函数体执行。

其中第2步和第3步由指令call一起执行。跳转到函数体之后开始执行函数,而i386函数体的”标准“开头是这样的(但也可以不一样):

push edp:将ebp压入栈中,忽然记起来好像学编译原理的时候有个什么”老sp“,这个书中给出的原文就是”old ebp“,我想翻译过来就是老ebp吧偷笑

mov ebp,esp:这是intel风格的汇编,将esp的值赋给ebp,这一步结束过后,ebp 也指向栈顶,同时此时的栈顶元素就是old ebp。

[可选] sub esp,XXX:在栈上分配XXX字节的临时空间。

[可选] push XXX:如有必要,保存名为xxx的寄存器(可重复多次)。对这些寄存器进行压栈操作是由于函数在运行过程中,会使用这些寄存器,因此会破坏这些寄存器中的值,因此为保护这部分数据,就先将这部分保存起来,待函数调用返回后再恢复。

在函数返回时,所进行的”标准“结尾与”标准“开头正好相反:

[可选] pop XXX:如有必要,恢复保存过的寄存器

mov esp,ebp:将ebp中的值赋给esp,则此时esp已经指向old ebp,此时标志着这个函数活动记录在栈中所占用的空间就被释放了。

pop ebp:从栈中将old ebp的值恢复到ebp中,此时ebp也指向old ebp。

ret:从栈中取得返回地址,并跳转到该位置。

好,让我们接下来看一个实际的例子:

#include <stdio.h>int foo1(int i);int foo2(int i);int main(){int x = 1;foo1(x);return 0;}int foo1(int i){int y = i;foo2(y);return y;}int foo2(int i){int z = i;return z;}

以上程序反汇编结果如下:

00000000004004f6 <main>:  4004f6:55                   push   %rbp  4004f7:48 89 e5             mov    %rsp,%rbp  4004fa:48 83 ec 10          sub    $0x10,%rsp  4004fe:c7 45 fc 01 00 00 00 movl   $0x1,-0x4(%rbp)  400505:8b 45 fc             mov    -0x4(%rbp),%eax  400508:89 c7                mov    %eax,%edi  40050a:e8 07 00 00 00       callq  400516 <foo1>  40050f:b8 00 00 00 00       mov    $0x0,%eax  400514:c9                   leaveq   400515:c3                   retq   0000000000400516 <foo1>:  400516:55                   push   %rbp  400517:48 89 e5             mov    %rsp,%rbp  40051a:48 83 ec 20          sub    $0x20,%rsp  40051e:89 7d ec             mov    %edi,-0x14(%rbp)  400521:8b 45 ec             mov    -0x14(%rbp),%eax  400524:89 45 fc             mov    %eax,-0x4(%rbp)  400527:8b 45 fc             mov    -0x4(%rbp),%eax  40052a:89 c7                mov    %eax,%edi  40052c:e8 05 00 00 00       callq  400536 <foo2>  400531:8b 45 fc             mov    -0x4(%rbp),%eax  400534:c9                   leaveq   400535:c3                   retq   0000000000400536 <foo2>:  400536:55                   push   %rbp  400537:48 89 e5             mov    %rsp,%rbp  40053a:89 7d ec             mov    %edi,-0x14(%rbp)  40053d:8b 45 ec             mov    -0x14(%rbp),%eax  400540:89 45 fc             mov    %eax,-0x4(%rbp)  400543:8b 45 fc             mov    -0x4(%rbp),%eax  400546:5d                   pop    %rbp  400547:c3                   retq     400548:0f 1f 84 00 00 00 00 nopl   0x0(%rax,%rax,1)  40054f:00 

先来分析最简单的foo2,与咱们分析的理论情况基本一样,首先是将rbp的值压入栈中,再来将rsp的值赋给rbp(注意objdump的结果是at&t风格的,因此源与目的操作数是相反的)。此处edi中存放着函数参数,首先赋给了-0x14(%rbp)这一地址,又将这一地址赋给了eax,这还没完,又把eax的值赋给了-0x4(%rbp)这个地址,再翻过头来又赋给了eax,返回值通过eax传递,一句能解决的事却反反复复做了四句。此时栈顶的元素还是old rbp,“pop %rbp”一方面将old ebp 重新赋给ebp,另一方面rsp所指向的值也变为返回地址。retq 一方面回到返回地址继续执行,另一方面也使rsp执行退栈操作,则此时栈已恢复成函数调用之前的情况。这里还有一点要注意的是“leaveq”这一句,这一句的作用其实就是

mov esp,ebppop ebp

这两句是我通过gdb跟踪寄存器值分析得到的。之所以foo2中不包括leaveq,可能是由于foo2中不包括临时变量,因此rsp并没有向下移动,因此rsp与rbp始终指向old ebp,因此就不需要mov esp,ebp 这一步,仅执行pop rbp 即可。

之所以会形成这样的函数活动记录,是因为在函数的调用方与被调用方之间存在着统一的理解,这个所谓的统一的理解就是所谓的“调用惯例”,一个调用管理主要由以下三个方面的内容组成:

  1. 函数参数的传递顺序和方式,x86-64已改为通过寄存器传递。
  2. 栈的维护方式,对于栈中压入数据的弹出工作既可以由函数调用方完成,也可以由函数本身完成。
  3. 名字修饰(name-mangling)的策略,为了链接的时候对调用惯例进行区分,调用管理要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。

讨论过了函数参数的传递,函数活动记录的形成与释放过程,接下来看看函数返回值的传递。

对于只有四字节的数据可以直接通过eax进行传递,对于返回5-8字节的数据,则采用eax与edx联合返回的形式,eax返回低4字节,edx返回高4字节。对于超过8字节的返回类型,请看如下分析,源代码如下:

typedef struct big_thing{char buf[128];}big_thing;big_thing return_test(){big_thing b;b.buf[0] = 0;return b;}int main(){big_thing n = return_test();}

反汇编结果如下,对于它的分析,就直接写在汇编代码里了:

0000000000400566 <return_test>:  400566:55                   push   %rbp  400567:48 89 e5             mov    %rsp,%rbp //前面这两句还是一般的函数开头  40056a:48 81 ec a0 00 00 00 sub    $0xa0,%rsp //通过使rsp减0xa0,以开辟空间  400571:48 89 bd 68 ff ff ff mov    %rdi,-0x98(%rbp) //此时rbp已经指向old rbp,rdi 为传入的参数,由于本函数没有参数,因此这个传入的参数实际上n的地址  400578:64 48 8b 04 25 28 00 mov    %fs:0x28,%rax  40057f:00 00   400581:48 89 45 f8          mov    %rax,-0x8(%rbp)  400585:31 c0                xor    %eax,%eax //这三句什么作用没看出来  400587:c6 85 70 ff ff ff 00 movb   $0x0,-0x90(%rbp) //b.buf[0] = 0,rbp-0x90为b的地址,可以通过gdb对以上数据进行验证  40058e:48 8b 85 68 ff ff ff mov    -0x98(%rbp),%rax //此时rbp-0x98中存储的是传入的参数的地址,而这个地址恰好是n的地址,最后这个地址又通过rax传回  400595:48 8b 95 70 ff ff ff mov    -0x90(%rbp),%rdx // 先传入rdx中  40059c:48 89 10             mov    %rdx,(%rax) //再传入rax中保存的地址中  40059f:48 8b 95 78 ff ff ff mov    -0x88(%rbp),%rdx //递减8个字节,并反复这一过程  4005a6:48 89 50 08          mov    %rdx,0x8(%rax)  4005aa:48 8b 55 80          mov    -0x80(%rbp),%rdx  4005ae:48 89 50 10          mov    %rdx,0x10(%rax)  4005b2:48 8b 55 88          mov    -0x78(%rbp),%rdx  4005b6:48 89 50 18          mov    %rdx,0x18(%rax)  4005ba:48 8b 55 90          mov    -0x70(%rbp),%rdx  4005be:48 89 50 20          mov    %rdx,0x20(%rax)  4005c2:48 8b 55 98          mov    -0x68(%rbp),%rdx  4005c6:48 89 50 28          mov    %rdx,0x28(%rax)  4005ca:48 8b 55 a0          mov    -0x60(%rbp),%rdx  4005ce:48 89 50 30          mov    %rdx,0x30(%rax)  4005d2:48 8b 55 a8          mov    -0x58(%rbp),%rdx  4005d6:48 89 50 38          mov    %rdx,0x38(%rax)  4005da:48 8b 55 b0          mov    -0x50(%rbp),%rdx  4005de:48 89 50 40          mov    %rdx,0x40(%rax)  4005e2:48 8b 55 b8          mov    -0x48(%rbp),%rdx  4005e6:48 89 50 48          mov    %rdx,0x48(%rax)  4005ea:48 8b 55 c0          mov    -0x40(%rbp),%rdx  4005ee:48 89 50 50          mov    %rdx,0x50(%rax)  4005f2:48 8b 55 c8          mov    -0x38(%rbp),%rdx  4005f6:48 89 50 58          mov    %rdx,0x58(%rax)  4005fa:48 8b 55 d0          mov    -0x30(%rbp),%rdx  4005fe:48 89 50 60          mov    %rdx,0x60(%rax)  400602:48 8b 55 d8          mov    -0x28(%rbp),%rdx  400606:48 89 50 68          mov    %rdx,0x68(%rax)  40060a:48 8b 55 e0          mov    -0x20(%rbp),%rdx  40060e:48 89 50 70          mov    %rdx,0x70(%rax)  400612:48 8b 55 e8          mov    -0x18(%rbp),%rdx  400616:48 89 50 78          mov    %rdx,0x78(%rax) //从rbp-0x90到rbp-0x18共0x78个字节,换为十进制就是120个字节,正好是struct的大小。  40061a:48 8b 85 68 ff ff ff mov    -0x98(%rbp),%rax //这一句有什么作用不太清楚,之前已经做这一操作了,而且寄存器的值也没有变化  400621:48 8b 4d f8          mov    -0x8(%rbp),%rcx  400625:64 48 33 0c 25 28 00 xor    %fs:0x28,%rcx //以上两句什么作用又不知道  40062c:00 00   40062e:74 05                je     400635 <return_test+0xcf> //跳过下一句  400630:e8 0b fe ff ff       callq  400440 <__stack_chk_fail@plt>  400635:c9                   leaveq //清栈操作  400636:c3                   retq   //返回0000000000400637 <main>:  400637:55                   push   %rbp  400638:48 89 e5             mov    %rsp,%rbp //还是函数的开头  40063b:48 81 ec 90 00 00 00 sub    $0x90,%rsp //开辟空间  400642:64 48 8b 04 25 28 00 mov    %fs:0x28,%rax  400649:00 00   40064b:48 89 45 f8          mov    %rax,-0x8(%rbp)  40064f:31 c0                xor    %eax,%eax //这几句的作用没有搞清楚  400651:48 8d 85 70 ff ff ff lea    -0x90(%rbp),%rax //n的地址与rbp-0x90的值相同  400658:48 89 c7             mov    %rax,%rdi //把这一地址作为参数传入rdi中  40065b:b8 00 00 00 00       mov    $0x0,%eax //这一句的作用没搞清楚  400660:e8 01 ff ff ff       callq  400566 <return_test>  400665:48 8b 55 f8          mov    -0x8(%rbp),%rdx  400669:64 48 33 14 25 28 00 xor    %fs:0x28,%rdx  400670:00 00   400672:74 05                je     400679 <main+0x42>  400674:e8 c7 fd ff ff       callq  400440 <__stack_chk_fail@plt>  400679:c9                   leaveq   40067a:c3                   retq     40067b:0f 1f 44 00 00       nopl   0x0(%rax,%rax,1)

通过以上分析我们可以发现x86-64中对于大对象的返回进行了一定的优化,直接将返回参数的地址作为函数的隐藏参数传入,在返回时将结果直接写入这一地址。

书中还给出了有关于c++的分析,有机会下次再给大家分析。

10.3 节主要对“堆”的概念及管理方法进行介绍。对于进程地址空间中的堆,其管理者就是运行库。其实对于“堆”空间的管理,程序可以直接将这项工作交给操作系统内核完成,而之所以操作系统内核并没有接手这项工作,而是将这项工作交给运行库进行,是由于如果程序频繁的使用系统调用,会造成很大的开销,因此以上方法并不可行。

首先来看看运行库是如何为程序分配堆空间的,本书中介绍的是使用brk() 与 mmap(),brk()的作用是调整数据段的结束地址,即它可以扩大或缩小数据段。将数据段的地址向高地址移动则相当于分配存储空间,而向低地址移动则相当于释放空间(实际处理上更加复杂)。mmap() 则首先申请一段虚拟地址空间,当文件不映射进这一内存区域时,我们称这块空间为匿名空间,这一部分空间被映射进入动态链接库映射区。

10.3.4 还介绍了三种堆分配算法,分别是“空闲链表”、“位图”、“对象池”。

最后给大家分享几篇博客

http://blog.csdn.net/g_brightboy/article/details/22793439

这一篇blog从概念上对c/c++中用到的动态内存管理的函数进行了介绍。

http://blog.chinaunix.net/uid-20786208-id-4979967.html

这一篇blog非常好,建议大家认真的读一读,这篇文章对glic2.21中malloc的源码进行了分析,我的电脑中安装的glibc的版本就是2.21

http://drops.wooyun.org/tips/6595

0 0
原创粉丝点击