从ELF文件结构看静态数组的构建

2015-08-08 01:57:17石峰范晓琴
电脑知识与技术 2015年15期
关键词:数组静态符号

石峰 范晓琴

摘要:在使用C++语言开发实时软件过程中,一般会使用静态数组在多个线程中共享数据。ELF文件是目前流行的Linux/Unix系统可执行文件的存储格式。使用C++语言开发的软件经过编译、链接以后会形成符合ELF文件格式的可执行文件存储在系统中。因此,研究ELF文件的结构可以清晰地呈现静态数组的构建原理。该文利用反汇编等手段分析几段C++代码,从编译和链接的角度研究了静态数组定义及运用过程,得到了关于静态数组构建的三条结论,并指出了在使用静态数组开发软件时需要注意的问题。

关键词:ELF文件;静态数组

中图分类号:TP312 文献标识码:A 文章编号:1009-3044(2015)15-0198-04

在使用C++语言开发实时软件过程中,经常需要在多个线程中共享数据。我们一般会定义一个静态类并将需要共享的数据定义为类的静态成员。由于静态类在程序运行时只有一个副本,非常类似C语言的全局变量,但与全局变量比较起来,使用类的静态成员具有信息隐蔽、避免命名冲突两个优势,非常适合多人协作式的开发模式。数组是一个单一类型对象的集合。使用静态数组成员可以将一组性质相同的数据传递到程序中。ELF文件是目前流行的Linux、Unix系统可执行文件的存储格式。它有四种类型:可重定位文件、可执行文件、共享目标文件、核心转储文件。使用C++语言开发的软件经过编译、链接以后会形成符合ELF文件格式的可执行文件存储在系统中。因此,研究ELF文件的结构可以清晰地呈现静态数组的构建原理。

本文利用反汇编等手段分析几段C++代码,从编译和链接的角度研究了静态数组定义及运用过程,得到了关于静态数组构建的三条结论,并指出了在使用静态数组开发软件时需要注意的问题。

1 ELF文件的总体结构

表1描述的是ELF目标文件的总体结构,其中省去了一些繁琐的结构,把重要的结构提取出来,形成了ELF文件的基本结构图。从表中可以看到,ELF文件的开头是个“头文件”,它描述了整个文件的属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息,文件头还包括一个段表(Section Table),段表其实是一个描述文件中各段的数组。段表描述了文件中各段在文件中的偏移位置及段的属性等,从段表里可以得到每个段的所有信息。文件头后面是各个段的内容,比如代码段(.text)保存的就是程序的指令,数据段(.data)保存的就是程序的静态变量。

2 分析ELF文件的常用工具

本文所分析的ELF文件都是基于64位Intel x86平台下Linux系统的ELF文件。因此会用到Linux系统中一些常用的命令及工具。第一个是gcc。Linux系统下功能强大、性能优越的编译器。gcc编译器能将C\C++语言源程序经过预处理、编译、汇编、链接四步,最终形成可执行文件。也可根据所选参数的不同,完成其中一个或几个阶段的工作。如有一个名为hello.cpp的c++源程序。运行$gcc hello.cpp –o hello 就可以生成名为hello的可执行文件。运行$gcc –c hello.cpp –o hello.o 就可以生成hello.o的目标文件。第二个是readelf。readelf命令可以用来显示ELF文件的信息。该命令有许多选项,可以显示ELF文件不同类型的信息。比如-a 选项可以显示ELF文件的全部信息。-S 可以显示ELF文件的段表;-s选项可以显示ELF文件的符号表;-r选项可以显示ELF文件的重定位表。第三个是objdump。是用来查看目标文件或可执行文件的文件构成的GCC工具。本文主要使用它来反汇编目标文件。第四个是gdb。GNU开源组织发布的一个强大的UNIX/Linux下的程序调试工具。

3 静态数组的定义与声明

在类cMyGlobal中定义一个整型静态数组global_array[5][20] 。如图1所示,头文件newglobal.h的第5行给出了数组的声明。静态变量和全局变量一样,在程序中只能有一次定义,这就意味着静态数据成员的定义不能放在头文件里。而应该放在含有类的非inline 函数定义的文件中。所以,在源文件newglobal.cpp的第2行至第6行给出了数组的定义部分,并对数组元素进行了初始化。接下来,运行gcc命令,生成目标文件newglobal.o。

ELF文件中的符号表往往是文件中的一个段,段名一般叫做“.symtab”。这个表里面记录了目标文件中所用到的所有符号。表中Num表示符号表数组的下标,从0开始,共8个符号。value是符号值即符号对应的虚拟地址。size是符号的大小即分配的存储空间。Type是符号的类型。Bind是符号的绑定情况,表示符号的局部或全局属性。Vis在目前的C++语言中没有使用。Ndx表示符号所在的段。Name就是符号的名字。静态数组global_array的绑定属性是GLOBAL,说明是全局可见的;它的Ndx属性为2,说明被定义在数据段;它的Size属性值为400,说明被分配了400字节的存储空间。由于c++编译器在将源文件编译成目标文件时,会将函数和变量的名字进行修饰,形成符号名。因此,数组global_array在目标文件中的符号名为_ZN9cMyGlobal12global_arr。值得注意的是类名cMyGlobal并没有作为一个单独的符号名出现,而是出现在数组global_array的符号名中,用来表示数组global_array是类cMyGlobal的数据成员。

4 静态数组的使用

对比newglobal.o(图2)与mainpro.o(图4)的符号表,可以看出,mainpro.o的符号表中_ZN9cMyGlobal12global_arr的Size属性为0,Type属性为NOTYPE,Ndx属性为UND,说明在mainpro.o中,符号_ZN9cMyGlobal12global_arr是一个外部定义的符号,也就是说关于它的定义在别的目标文件中(newglobal.o)。

mov %eax -0x4(%rbp) 这5段代码对应着源文件中静态数组cMyGlobal::global_array对变量test_var的五次赋值。寄存器%rip为指令计数器,代表下一条要执行的指令的地址。0(%rip)表示的地址就是数组元素所在的地址。C++代码在目标文件阶段各符号的地址是不确定的,经过链接之后才会被赋予确定的地址,因此用0表示,并不代表不同的数组元素拥有相同的地址。运行命令readelf可已看到目标文件的重定位表(图6)。

最后一列是符号名加偏移量。静态数组global_array是一个整型数组,每个元素占4个字节。以元素cMyGlobal::global_array[1][0]为例,该元素为整型数组的第20个元素。所以元素cMyGlobal::global_array[1][0]的首地址相对于数组首地址的偏移量为0x50。该元素位于代码段首地址偏移0x1f的位置,正好是第三个0(%rip)在代码段的位置。综合以上分析,静态数组cMyGlobal::global_array的五个元素在目标文件的代码段映射为五个待重定位的地址。下一步,运行命令g++ 生成最终的可执行文件。

5 一种编码错误的分析

为了说明本节内容,对newglobal.h的代码进行修改。将图1中newglobal.h的第1行#define NUM 20改为#define NUM 10并存储为文件oldglobal.h。这么做的目的是将静态数组global_array声明为5*10的二维数组。将图3中mainpro.cpp的第2行#include "newglobal.h"改为#include "oldglobal.h"。运行命令 g++生成可执行文件oldpro。

可以看到变量test_var分别被赋值为1、2、80、81、82。结合对静态数组global_array定义部分的分析,第二次输出了数组[0][1]、[0][2]、[0][10]、[0][11]、[0][12]的内容,并不是期望的[0][1]、[0][2]、[1][0]、[1][1]、[1][2]。出现这样的问题很明显是因为引用了不同的头文件。再看图4中mainpro.o的符号表。程序在编译阶段,编译系统把符号_ZN9cMyGlobal12global_arr理解为外部符号,既_ZN9cMyGlobal12global_arr的定义在别的目标文件里,在链接阶段才会对该符号进行决议。而_ZN9cMyGlobal12global_arr真正定义在newglobal.o中。编译程序在编译源程序mainpro.cpp时,只是知道_ZN9cMyGlobal12global_arr是个外部变量,并不会对符号的类型进行检查。在编译代码的过程中,数组的元素被直接根据声明的维数转化为待重定位的地址。在链接阶段地址是符号_ZN9cMyGlobal12global_arr确定的,和声明时数组的大小维数没有任何关系,甚至和数据类型也没有任何关系。于是就出现了虽然数组的声明改变了,但由于数组的定义保持不变,程序输出了错误数据的情况。

6 结论

1)编译程序对源文件编译后形成目标文件。在目标文件的代码段中,静态数组的元素根据声明的维数转化为待重定位的虚拟地址,没有下标的概念。

2)编译程序在编译源文件时,对源文件引用的外部变量不会进行类型检查。

3)在程序的链接阶段,编译程序根据目标文件符号表中的符号来确定代码段静态数组需要重定位的首地址。与声明的数组大小没有关系。

通过本文分析,得到了静态数组的三条结论。在软件开发过程中,引用静态数组一定要确定引用了正确的头文件。另一方面,更改了静态数组的定义,一定要对使用它的程序重新编译,以免因为目标文件版本不一致导致程序执行出错。

参考文献:

[1] Lippman S B.C++ Primer[M]. 潘爱民,张丽, 译. 3rd ed. 北京: 中国电力出版社, 1998.

[2] 俞甲子, 石凡, 潘爱民. 程序员的自我修养[M]. 北京: 电子工业出版社, 2012.

猜你喜欢
数组静态符号
JAVA稀疏矩阵算法
电脑报(2022年13期)2022-04-12 00:32:38
学符号,比多少
幼儿园(2021年6期)2021-07-28 07:42:14
静态随机存储器在轨自检算法
JAVA玩转数学之二维数组排序
电脑报(2020年24期)2020-07-15 06:12:41
“+”“-”符号的由来
变符号
寻找勾股数组的历程
图的有效符号边控制数
机床静态及动态分析
机电信息(2015年9期)2015-02-27 15:55:56
具7μA静态电流的2A、70V SEPIC/升压型DC/DC转换器