一、属性声明
1、存储段:section
1.1 GNU C编译器扩展关键字:__attribute__
GNU C增加了一个__attribute__
关键字用来声明一个函数、变量或类型的特殊属性。主要用途就是指导编译器在编译程序时进行特定方面的优化或代码检查。例如,我们可以通过属性声明来指定某个变量的数据对齐方式。
__attribute__
的使用非常简单,当我们定义一个函数、变量或类型时,直接在它们名字旁边添加下面的属性声明即可。
需要注意的是,__attribute__
后面是两对小括号,不能图方便只写一对,否则编译就会报错。括号里面的ATTRIBUTE表示要声明的属性。目前__attribute__
支持十几种属性声明。
- section.
- aligned.
- packed.
- format.
- weak.
- alias.
- noinline.
- always_inline.
aligned和packed用来显式指定一个变量的存储对齐方式。在正常情况下,当我们定义一个变量时,编译器会根据变量类型给这个变量分配合适大小的存储空间,按照默认的边界对齐方式分配一个地址。而使用__atttribute__这个属性声明,就相当于告诉编译器:按照我们指定的边界对齐方式去给这个变量分配存储空间。
有些属性可能还有自己的参数。如aligned(8)表示这个变量按8字节地址对齐,属性的参数也要使用小括号括起来,如果属性的参数是一个字符串,则小括号里的参数还要用双引号引起来。
1.2 属性声明:section
section属性的主要作用是:在程序编译时,将一个函数或变量放到指定的段,即放到指定的section中。一个可执行文件主要由代码段、数据段、BSS段构成。除了这三个段,可执行文件中还包含其他一些段。用编译器的专业术语讲,还包含其他一些section,如只读数据段、符号表等。
在Linux环境下,使用GCC编译生成一个可执行文件a.out,使用readelf命令,就可以查看这个可执行文件中各个section的基本信息,如大小、起始地址等。在这些section中,.text section就是我们常说的代码段,.data section是数据段,.bss section是BSS段。
编译器在编译程序时,以源文件为单位,将一个个源文件编译生成一个个目标文件。在编译过程中,编译器都会按照这个默认规则,将函数、变量分别放在不同的section
中,最后将各个section
组成一个目标文件。编译过程结束后,链接器会将各个目标文件组装合并、重定位,生成一个可执行文件。
在GNU C中,我们可以通过__attribute__
的section
属性,显式指定一个函数或变量,在编译时放到指定的section
里面。通过上面的程序我们知道,未初始化的全局变量默认是放在.bss section中的,即默认放在BSS
段中。现在我们就可以通过section
属性声明,把这个未初始化的全局变量放到数据段.data
中。
通过readelf命令查看符号表,我们可以看到,uninit_val
这个未初始化的全局变量,通过__attribute__((section(".data")))
属性声明,就和初始化的全局变量一样,被编译器放在了数据段.data
中。
1.3 U-boot镜像自复制分析
有了section这个属性声明,我们就可以试着分析:U-boot在启动过程中,是如何将自身代码加载的RAM中的
。
嵌入式Linux中,U-boot的用途主要是加载Linux内核镜像到内存,给内核传递启动参数,然后引导Linux操作系统启动。U-boot一般存储在NOR Flash或NAND Flash上。无论从NOR Flash还是从NAND Flash启动,U-boot其本身在启动过程中,都会从Flash存储介质上加载自身代码到内存,然后进行重定位,跳到内存RAM中去执行。
那么U-boot是怎么完成代码自复制的呢?或者说它是怎样将自身代码从Flash复制到内存的呢?
在复制自身代码的过程中,一个主要的疑问就是:U-boot是如何识别自身代码的?是如何知道从哪里开始复制代码的?是如何知道复制到哪里停止的?这时候我们需要了解U-boot源码中的一个零长度数组。
这两行代码的作用是分别定义一个零长度数组,并指示编译器要分别放在.__image_copy_start和.__image_copy_end这两个section中。
链接器在链接各个目标文件时,会按照链接脚本里各个section的排列顺序,将各个section组装成一个可执行文件。
通过链接脚本我们可以看到,__image_copy_start
和__image_copy_end
这两个section
,在链接的时候分别放在了代码段.text的前面、数据段.data的后面,作为U-boot复制自身代码的起始地址和结束地址。**而在这两个section中,我们除了放两个零长度数组,并没有放其他变量,众所周知,零长度数组是不占用存储空间的。**因此以上两个零长度数组分别代表了U-boot镜像要复制自身镜像的起始地址和结束地址。无论U-boot自身镜像存储在NOR Flash,还是存储在NAND Flash上,只要知道了这两个地址,我们就可以直接调用相关代码复制。
在嵌入式系统中,通过ARM的LDR伪指令,直接获取要复制镜像的首地址,并保存在R1寄存器中。数组名本身其实就代表一个地址,通过这种方式,U-boot在嵌入式启动的初始阶段,就完成了自身代码的复制工作:从Flash复制自身镜像到内存中,然后进行重定位,最后跳到内存中执行。
2、属性声明:aligned
2.1 地址对齐:aligned
GNU C通过__attribute__
来声明aligned
和packed
属性,指定一个变量或类型的对齐方式。这两个属性用来告诉编译器:在给变量分配存储空间时,要按指定的地址对齐方式给变量分配地址。
定义一个int
变量,在内存中以8
字节地址对齐,就可以这样定义。

指定结构体按8
字节地址对齐,所以编译器要在结构体后面填充1
字节,这样整个结构体的大小就变为8
字节,按8
字节地址对齐。
3、format
3.1 变参函数的格式检查
GNU通过__attribute__
扩展的format属性,来指定变参函数的参数格式检查。使用方法如下。
在一些商业项目中,我们经常会实现一些自定义的打印调试函数,甚至实现一个独立的日志打印模块。这些自定义的打印函数往往是变参函数,用户在调用这些接口函数时参数往往不固定,那么编译器在编译程序时,怎么知道我们的参数格式对不对呢?
__attribute__
的format
属性这时候就派上用场了。上面的示例代码中,我们定义一个LOG()
变参函数,用来实现日志打印功能。
编译器在编译程序时,如何检查LOG()函数的参数格式是否正确呢?通过给LOG()函数添加__attribute__((format(printf,1,2)))
属性声明就可以了。这个属性声明告诉编译器:你知道printf()函数不?你怎么对printf()函数进行参数格式检查的,就按照同样的方法,对LOG()函数进行检查。
属性format(printf,1,2)有3个参数,第1个参数printf是告诉编译器,按照printf()函数的标准来检查;第2个参数表示在LOG()函数所有的参数列表中格式字符串的位置索引;第3个参数是告诉编译器要检查的参数的起始位置。
在这个LOG()函数中有2个参数,第1个参数是格式字符串,第2个参数是要打印的一个常量值0,用来匹配格式字符串中的占位符。
4、weak
在C语言标准中,当我们定义并初始化一个数组时,常用方法如下。