工欲善其事必先利其器-C语言拓展–嵌入式C语言(五)
属性声明:section
我知道不耻下问是个好事,而且身边那么多大牛,不能算不耻,但是把一些能自己学习搞定的事情,拿去打扰别人,我觉得一点也不酷。
GNU C编译器扩展关键字:attribute
__attribute__关键字用来声明一个函数、变量或类型的特殊属性。
这个特殊属性是什么?
作用就在于让编译器在编译程序的时候进行特定方面的优化或者代码检查。
话不多说整个栗子:
__attribute__的使用非常简单–>直接在它们名字旁边添加下面的属性声明即可。
__attribute__((ATTRIBUTE))
告诉编译器 有ATTRIBUTE这个属性了,给我用起来
目前属性有十几种:
● section. ● aligned. ● packed. ● format. ● weak. ● alias. ● noinline. ● always_inline. ● ……
其中aligned和packed用来显式的指定一个变量的存储对齐的方式。正常的情况下,我们定义一个变量的时候,编译器会根据变量类型给这个变量分配合适大小的存储空间,按照默认的边界对齐方式分配一个地址。而使用__atttribute__这个属性声明,就相当于告诉编译器:按照我们指定的边界对齐方式去给这个变量分配存储空间。
举个栗子:
char c2 __attribute__((aligned(8)) = 4; int global_val_attribute__((section(".data")));
**有些属性可能还有自己的参数。**如aligned(8)表示这个变量按8字节地址对齐,属性的参数也要使用小括号括起来,如果属性的参数是一个字符串,则小括号里的参数还要用双引号引起来。
(section就在屁屁后面,马上要讲的)
当对一个变量添加多个属性说明的时候,定义变量的时候,各个属性之间用都好隔开。
属性声明要紧挨着变量 – >
这个就是错误的哦
到这里我们知道了怎么去把属性用起来,让编译器听话点,下面我们来看看这些属性到底有什么。
属性声明:section
section其实如果你有过汇编的基础,会对这个有个段这个概念。当然没有也没什么影响。
section属性作用:在程序编译的时候,将一个函数或者变量放到指定的段,即放到指定的section中。
在一个可执行文件组成:代码段、数据段、BSS段构成。
代码段:编译生成的可执行指令代码
数据段+BSS段:存放全局变量、未初始化的全局变量
当然还有其他的section,比如只读数据、符号表等。
使用readelf命令可以去查看一个可执行文件的各个section的信息。
我们知道,一段源程序代码在编译生成可执行文件的过程中,函数和变量是放在不同段中的。一般默认的规则如表6-1所示。
这些都是可以通过命令看到的
readelf -s *.out-->查看符号表 readelf -S *.out -->查看section header表
最后通过符号表和section header就能看到.c文件你写的变量、函数等等在哪个段。
编译器在编译程序时,以源文件为单位,将一个个源文件编译生成一个个目标文件。
在编译过程中,编译器都会按照这个默认规则,将函数**、变量分别放在不同的section**中,最后将各个section组成一个目标文件。
编译过程结束后,链接器会将各个目标文件组装合并、重定位,生成一个可执行文件。
这里想不想让每个班先整理好,然后年级整理的时候直接组装。
而这些由编译器默认分发组装section的事情,我们可以通过section这个属性进行自定义。
通过__attribute__的section属性,显式指定一个函数或变量,在编译时放到指定的section里面。
int global_val = 8; int uninit_val __attribute__((section(".data")));
按说这个uninit_val这个未初始化应该到BSS段,但是这里我们显示指定以后,这个玩意就到了.data 数据段
既然被你知道了这么厉害的,那我们来举个在内核中应用这个section的栗子
U-boot镜像自复制分析
我们来看看这个U-boot在启动过程中是如何将自身代码加载到RAM中的。
(这个是不是就是那个传说的动作自举?问句哈)
U-boot:加载linux内核镜像到内存,给内核传递启动参数,然后引导Linux操作系统启动。
一种bootloader,有必要的话改天整篇聊聊这个玩意诶。
U-boot一般存储在NOR Flash或NAND Flash上。无论从NOR Flash还是从NAND Flash启动,U-boot其本身在启动过程中,都会从Flash存储介质上加载自身代码到内存,然后进行重定位,跳到内存RAM中去执行。(励志啊)
(因为CPU是与内存进行交互的)
在复制自身代码的过程中,一个主要的疑问就是:
U-boot是如何识别自身代码的?
是如何知道从哪里开始复制代码的?
是如何知道复制到哪里停止的?
(这特么是一个问题?)
这个时候我们不得不说起U-boot源码中的一个零长度数组。
这两行代码定义在U-boot-2016.09中的arch/arm/lib/section.c文件中
这两行代码的作用是分别定义一个零长度数组,并指示编译器要分别放在.__image_copy_start和.__image_copy_end这两个section中。
链接器在链接各个目标文件时,会按照链接脚本里各个section的排列顺序,将各个section组装成一个可执行文件。
U-boot的链接脚本U-boot.lds在U-boot源码的根目录下面。
看看里面长啥样:
__image_copy_start:代码段.text的前面
__image_copy_end:数据段.data的后面
作为U-boot复制自身代码的起始地址和结束地址。
因为零长数组是不占位置的,所以这个就代表了U-boot镜像复制的起始与终点地址。
(好嘛这样想来前面确实是一个问题。)
无论U-boot自身镜像存储在NOR Flash,还是存储在NAND Flash上,只要知道了这两个地址,我们就可以直接调用相关代码复制。在arch/arm/lib/relocate.S中,ENTRY(relocate_code)汇编代码主要完成代码复制的功能。
在这段汇编代码中,寄存器R1、R2分别表示要复制镜像的起始地址和结束地址,R0表示要复制到RAM中的地址,R4存放的是源地址和目的地址之间的偏移,在后面重定位过程中会用到这个偏移值。在汇编代码中:
通过ARM的LDR伪指令,直接获取要复制镜像的首地址,并保存在R1寄存器中。数组名本身其实就代表一个地址,通过这种方式,U-boot在嵌入式启动的初始阶段,就完成了自身代码的复制工作:从Flash复制自身镜像到内存中,然后进行重定位,最后跳到内存中执行。
嵌入式启动的时候就开始运行relocate.S这个文件,然后搬动自己。
我在阅读linux内核艺术的时候,发现这个内核对于这个内存算的很精细,同时对于内核的启动加载,整个流程是非常精细。
有时候有种那种你站在一个凉席,你边走边卷起来的感觉,反正很有意思。