我们已经知道了虚拟内存由用户空间和内核空间两部分组成。内核空间存放的是操作系统的数据,这一块空间对于应用程序来说是没有权限进行操作的,应用程序能操作的只有用户空间,所以本节对用户空间做进一步介绍。
操作系统能执行的文件都要符合一定的格式,比如Windows系统的.exe程序文件、.dll库文件都是PE(Portable Executable,可移植可执行)文件。Linux系统中可执行的文件(包括.o可重定位文件、.so库文件等)都是ELF(Executable and Linkable Format,可执行和链接格式)文件。系统要执行某个程序,首先要将程序中的数据加载进用户空间的虚拟内存块中,所以想要了解用户空间中有哪些数据,就需要先了解ELF文件格式。ELF文件格式如图1-3所示。
图1-3 ELF文件格式
ELF文件一般由ELF头(ELF Header)、数据段(Section)、段头表(Section Header Table)和程序头表(Program Header Table)组成,它们的解释见表1-1。
表1-1 ELF文件的组成部分解释
接着,我们再详细了解一下数据段和程序段。
1.数据段
通过Android NDK中提供的readelf工具,执行readelf-S libart.so命令来读取libart库的数据段信息,如图1-4所示,可以看到art虚拟机库文件有30多个数据段。
图1-4 libart库的数据段信息
由于Section段的数目比较多,这里仅对一些常见的数据段进行介绍。
❑代码段(.text):包含了可执行程序的机器指令。在运行时,该段的内容被加载到内存中,并由处理器执行。代码段通常具有可执行和只读权限。
❑数据段(.data):包含了程序的全局和静态变量的初始化值。在运行时,该段的内容被加载到内存中可以进行读写的区域,因此数据段通常具有可读写权限。
❑BSS段(.bss,Block Started by Symbol):用于存储程序中未初始化的全局和静态变量。在运行时,该段的内容会被初始化为0或空值。BSS段通常具有可读写权限。
❑只读数据段(.rodata):包含了程序中的只读常量数据,如字符串常量、常量表等。在运行时,该段的内容被加载到内存中,并具有只读权限。
❑调试信息段(.debug):包含了用于调试和符号解析的信息,如源码行号、变量名、函数名等。该段通常在发布版本中被剥离,以减小文件大小(基于安全、体积等因素考虑,线上的so文件中一般都会剔除debug段,所以图1-4中未见到debug段)。
❑动态段(.dynamic):该段主要包含了外部依赖库的信息,比如外部库的名称、外部库函数的地址等。
❑符号段(.symtab):该段主要包含了程序中的符号信息。符号信息包括符号的名称、类型、大小、值、段等,它们可以用于调试、链接、反汇编等。后文要介绍的一些技术方案会用到符号,所以这里重点讲解一下什么是符号。编译器在将C++源代码编译成目标文件时,会对函数和变量的名字进行修饰,并生成对应的符号名。编译器不同,生成的符号也不一样,通过GCC编译器编译示例函数生成的对应符号见表1-2。
表1-2 示例函数及其对应的符号
这里以int Test::func(int)函数为例来进行讲解。GCC在生成方法的符号时,都以_Z开头,对于嵌套的名字后面紧跟N,然后是各个名称空间和类的名称长度及名称,所以是4Test4func,嵌套的方法以E表示结尾,非嵌套的方法则不需要用E表示结尾,最后是入参类型,所以这个函数的符号连起来就是_ZN4Test4funcEi。这些符号信息会绑定对应的类型、信息、地址等属性,形成符号条目并存放在符号表中。符号表可以帮助我们调试和定位程序运行中的问题。但出于对包体积和安全的考虑,线上运行时我们往往会把so库中的符号表移除。
2.程序段
我们再通过readelf工具执行readelf-l libart.so命令来读取libart库的程序段信息,如图1-5所示,可以看到art虚拟机库文件将31个数据段(Section)组织成了9个程序段(Program)。
图1-5 libart库的程序段信息
3.虚拟内存的结构
系统执行ELF格式的程序文件时,会将ELF文件中的数据段按照程序头组织的顺序加载进虚拟内存中,并放在低地址区域,即虚拟内存地址从0开始的区域。
当ELF文件中的数据在虚拟内存中存放好后,就要用到栈空间和堆空间了。其中,栈空间由编译器自动分配和释放,用于在函数执行时存放函数的参数值、局部变量、执行指令等;堆空间用于内存的动态分配,可以由开发者自己分配和释放,主要由malloc和free函数实现。但通过Java或者Kotlin进行Android开发时,不需要我们手动分配和释放堆空间的内存,因为虚拟机程序已经帮我们做了。堆内存的地址分配是从下到上的,栈内存的地址分配是从上到下的,这种相向的分配方式可以充分利用内存空间,因为如果堆空间和栈空间同时向一个方向分配内存,那么堆空间就必然要限制在一个固定的大小,以防止堆空间申请内存时的地址越界到栈空间。
栈空间再往上便是内核空间,用于存放操作系统的数据。图1-6是32位操作系统下ELF文件与虚拟内存的结构模型,通过该模型,我们可以对虚拟内存的结构有更加清晰的理解。
图1-6 ELF文件与虚拟内存的结构模型