个人单页网站,网站流量查询平台,建筑设计公司经营范围有哪些,wordpress建站怎么样上一篇文章记录了GDB调试从入门到熟练掌握的学习全过程。点击链接查看#xff1a;【软件开发底层知识修炼】十九 GDB调试从入门到熟练掌握超级详细实战教程学习目录 还记得在以前的学习Binutils工具的时候#xff0c;学习了很多工具来查看可执行程序的结构#xff0c;那个时… 上一篇文章记录了GDB调试从入门到熟练掌握的学习全过程。点击链接查看【软件开发底层知识修炼】十九 GDB调试从入门到熟练掌握超级详细实战教程学习目录 还记得在以前的学习Binutils工具的时候学习了很多工具来查看可执行程序的结构那个时候并没有详细说明程序的结构今天就来学。下面是之前学习的Binutils工具集的几篇文章可以参考学习【软件开发底层知识修炼】六 Binutils辅助工具之- addr2line与strip工具【软件开发底层知识修炼】七 Binutils辅助工具之- ar工具与nm工具【软件开发底层知识修炼】八 Binutils辅助工具之- objdump工具 与 sizestrings工具 本篇文章开始学习可执行程序的结构。也就是我们平时说的可执行文件的结构。本文不会像《程序员的自我修养》那样详细解释可执行文件的每一个细节我希望通过这次学习能够对程序的结构有一个永久性的认知。当然还是建议要把《程序员的自我修养》仔细阅读完。 文章目录1 程序是由不同的段构成的1.1 代码示例2 程序中的栈结构3 堆Heap的简要概述4 内存映射段mmap4 总结 1 程序是由不同的段构成的 至于什么是段如果看了《X86汇编语言-从实模式到保护模式》应该会非常清楚。段不过就是一段内存结构。把类似的指令放到连续的内存区域就是代码段.text段把初始化了的数据放到连续的内存区域就是数据段.data段把未初始化的数据放在一起就是.bss段 程序的静态特征是指令和数据实际上就是一堆二进制放在那里磁盘不动。程序的动态特征就是执行指令来处理数据实际上就是把磁盘上的二进制文件加载到内存让指令来处理数据。
那么你用C语言写一个代码它与可执行文件的内部结构是如何对应的? 代码段.text 源代码中的可执行语句编译后进入代码段代码段在内存管理单元的系统属性中具有只读属性它是不可写的。代码段的大小在编译结束后大小就直接确定了运行的过程中不会被改变代码段中可以包含常量数据如字符串常量 数据段.data , .bss, .rodata 数据段中用于存放源代码中具有全局生命周期的变量。不具有全局生命期的局部非静态变量不在数据段中而是在栈中。栈后面会讲。 .bss .bss是存储未初始化的变量。或者说初始化为0的变量 .data .data存储的是具有非0初始值的变量 .rodata .rodata存储的是const修饰的变量 为什么同是全局变量和静态局部变量为什么初始化的和未初始化的变量放在不同的段中 可以这样想有初始化值的变量在可执行文件中就直接将它的值保存到文件中加载到内存的时候直接将变量对应的值也加载到内存中。而未初始化的变量或者本来就赋值为0的变量在可执行文件中不用保存初始值这减小了可执行文件的体积将其加载到内存中时啥也不管直接全部赋值为0这也也可以提高加载的效率。总结来说就是以下两点 .bss段在可执行文件中不赋初值。在加载到内存中时直接全部初始化为0。这样减少了可执行文件的体积也提高额加载的效率 .data段中在可执行文件中直接将变量对应的初始值保存加载到内存中时直接将文件中对应的值加载到内存中即可。这也提高了程序的加载效率。 文件头File header 文件头并不是今天的重点。简单来说文件头中保存了程序的各个段的信息操作系统加载程序的时候首先要读取这个文件头先计算出各个段的大小才能从磁盘中准确的读取相应的执行与数据。 并且在文件头中也记录了类似于符号符号变之类的信息。这些不再多讲。 1.1 代码示例
下面我们写一个代码来使用一些具体的工具查看各个段。
test.c
char g_no_val; // .bss 1byte
int g_value1; //.data 4byte
char g_str[]D.T.SoftWare_lyy; // .data 17byte
const int g_const3; //.rodata 4byteint dt_main(){static char c_no_value; // .bss 1bytestatic int c_value2; // .data 4bytereturn 0;
}可以看到上述程序没有main函数但是我们可以指定dt_main()函数为入口函数。使用以下方式进行编译gcc -e dt_main -nostartfiles test.c -o test.out得到可执行代码文件test.out使用下面的命令查看它的各个段的信息objdump -h test.out 由上图可以看到各个段的大小起始段的地址等.data段大小0x1c28字节我们由上述代码可以看到.data段的变量一共是25字节由于对其最终是28字节也就是16进制的1c。.data段的起始地址是08049ff4.bss段大小0x44字节这个很明显。.bss段的起始地址是0804a010.rodata段大小4字节这个也很明显。起始地址为0804819c 使用nm test.out 查看各个符号的属性 因为符号g_const所在的.rodata段只有它一个所以它的地址自然就是.rodata段的起始地址。如上图 其他的信息也很容易看懂。这里不再赘述。 我们还可以使用命令objdump -s -j .rodata test.out 查看某一个段的信息
2 程序中的栈结构
在最开始的那张图
我们始终没有说ab这两个变量在哪里。它们不在上述的那些段中。当然它们肯定也是在某一块内存中的。这块内存我们叫做栈。
对于栈我们这也不说特别详细值说明栈的基本用处。 栈的本质是一块连续的内存结构。它与数据结构中的栈不是一个概念但是操作很像。其中SP寄存器当然这是最基本的16位的32位的叫ESP存的是栈顶的指针。它用于栈的入栈操作与出栈操作。栈的增长方向一般是向下。这与下面即将要说的堆内存正好相反。 栈一般有什么用处呢 除了保存类似于上述的ab这种局部变量以外。还有以下几种用途。 中断发生时栈用于保存一些寄存器的值。函数调用时栈用于保存函数的上下文信息活动记录依然是一些寄存器的值等并发编程时每一个线程都有一个自己独立的栈。说是独立有点不恰当其实它们都是在一个大的独立的进程空间中。 在本文就不打算再详细说栈这种结构与作用。可以参考《程序员的自我修养》
知道了栈结构理应还要知道堆结构。它们总是放到一起做对比。
3 堆Heap的简要概述
在这里我们知道堆是用于以下用途即可具体的后面还会学习。
堆是一片闲置的内存空间用于程序在运行的时候动态分配的堆空间的分配需要函数的支持比如malloc。C的new关键字底层也是调用相应的函数堆空间的使用后需要显示的释放栈就不需要一般是用free。C中为delete。不过更加高级的语言具有垃圾回收机制比如java
4 内存映射段mmap
上述学习了各个段以及堆结构与栈结构。
还有一种内存段叫做内存映射段。它是用来做什么的呢
如果了解动态链接的过程应该知道如果程序加载时需要动态链接相关动态库的话操作系统内核会将相应的动态库的文件直接映射到内存中。映射的位置可以称为内存映射段。
还有一种情况是如果想要读取一个文件的内容一点一点的读开销总是相当大。如果操作系统内核直接将文件映射到某一块内存再来读取文件的内容将会块的多。
还有一种是程序执行时可以创建匿名映射区来存放程序数据。什么是匿名映射区比如一个程序生产了很多数据要将它最终存到一个文件中。那么不可能说生产一个数据就存放到文件中更加高效的做法是在内存中创建一个赋值全为0的区域将生产的数据暂时先存放到这里生产完或者生产了足够多的数据后再将数据写到磁盘上的文件。这就是匿名映射区。毕竟磁盘的读写都是以扇区为单位一个扇区大小为512字节一次写多一点数据总是比你一次写1字节的数据更加高效。 有一点要明白将文件的内容映射到内存实际上是映射到进程的虚拟地址空间而且映射的过程是没有数据的迁移的也就是没有数据的拷贝。 其实上面我们并没有说明白为什么将文件映射到内存后再读写会快一些。 如果使用正常的read函数进行读文件是需要两次的数据拷贝一次是内核从磁盘将文件的内容拷贝到内核地址空间然后再从内核地址空间拷贝到用户地址空。这里进行了两次数据的拷贝。开销比较大
但是如果是使用内存映段的话就不一样了
如下图: 首先将硬盘上的文件数据从逻辑上映射到内存中这没有数据拷贝零耗时。当用户程序读数据的时候从虚拟地址空间读通过缺页中断进行文件数据的实际载入这里就是真正的数据的拷贝从文件中拷贝到真实的物理内存这里要注意一点映射后的内存虚拟内存的读写就是对文件数据的读写。
4 总结
其实这些内容以前都见过学过。下面给一个大的进程的虚拟地址空间的内存分配图 当然在Linux系统中内核与用户空间的比例是1:3但是在windows系统中就是2:2了. 本文章参考狄泰软件学院相关课程 想学习的可以加狄泰软件学院群 群聊号码199546072 学习探讨加个人可以免费帮忙下载CSDN资源 qq1126137994 微信liu1126137994 学习交流资源分享qq群962535112