找回密码
 立即注册
Qt开源社区 门户 查看内容

嵌入式C语言自我修养 06:U-boot镜像自拷贝分析 - section属性

2019-2-13 21:53| 发布者: admin| 查看: 700| 评论: 0

摘要: 作者:宅学部落 6.1 GNU C 的扩展关键字:__attribute__GNU C 增加一个 __attribute__关键字用来声 ...
作者:宅学部落

6.1 GNU C 的扩展关键字:__attribute__


GNU C 增加一个 __attribute__关键字用来声明一个函数、变量或类型的特殊属性。声明这个特殊属性有什么用呢?主要用途就是指导编译器在编译程序时进行特定方面的优化或代码检查。比如,我们可以通过使用属性声明指定某个变量的数据边界对齐方式。

__attribute__的使用非常简单,当我们定义一个函数、变量或类型时,直接在它们名字旁边添加下面的属性声明即可:
__atttribute__((ATTRIBUTE))
这里需要注意的是:__attribute__后面是两对小括号,不能图方便只写一对,否则编译可能通不过。括号里面的 ATTRIBUTE 代表的就是要声明的属性。现在 __attribute__支持十几种属性:

  • section

  • aligned

  • packed

  • format

  • weak

  • alias

  • noinline

  • always_inline

  • ……

在这些属性中,aligned 和 packed 用来显式指定一个变量的存储边界对齐方式。一般来讲,我们定义一个变量,编译器会根据变量类型,按照默认的规则来给这个变量分配大小、按照默认的边界对齐方式分配一个地址。而使用 __attribute__这个属性声明,就相当于告诉编译器:按照我们指定的边界地址对齐去给这个变量分配存储空间。
charc2__attribute__((aligned(8)) =4;
intglobal_val__attribute__((section(".data")));
有些属性可能还有自己的参数。比如 aligned(8) 表示这个变量按8字节地址对齐,参数也要使用小括号括起来。如果属性的参数是一个字符串,小括号里的参数还要用双引号引起来。

当然,我们也可以对一个变量同时添加多个属性说明。在定义时,各个属性之间用逗号隔开就可以了。



在上面的示例中,我们对一个变量添加2个属性声明,这两个属性都放在 __attribute__(()) 的2对小括号里面,属性之间用逗号隔开。这里还有一个细节,就是属性声明要紧挨着变量,上面的三种定义方式都是没有问题的,但下面的定义方式在编译的时候可能就通不过。
charc2=4__attribute__((packed,aligned(4)));

6.2 属性声明:section


在本节教程中,我们先讲一下 section 这个属性。使用__attribute__来声明一个 section 属性,主要用途是在程序编译时,将一个函数或变量放到指定的段,即 section 中。

在讲解这个功能之前,为了照顾一下对计算机编译、链接过程不是很了解的同学,我们先讲一讲程序的编译、链接过程。


程序的编译、链接过程


一个可执行目标文件,它主要由代码段、数据段、BSS 段构成。代码段主要存放编译生成的可执行指令代码,数据段和 BSS 段用来存放全局变量、未初始化的全局变量。代码段、数据段和 BSS 段构成了一个可执行文件的主要部分。

除了这三个段,可执行文件中还包含其它一些段。用编译器的专业术语讲,还会包含其它一些 section,比如只读数据段、符号表等等。我们可以使用下面的 readelf 命令,去查看一个可执行文件中各个 section 的信息。



在 Linux 环境下,使用 GCC 编译生成一个可执行文件 a.out,使用上面的 readelf 命令,就可以查看这个可执行文件中各个 section 的基本信息,比如大小、起始地址等等。在这些 section 中,其中 .text section 就是我们常说的代码段,.data section 是数据段,.bss section 是 BSS 段。

我们知道一段源程序代码在编译生成可执行文件的过程中,函数和变量是放在不同段中的。一般默认的规则如下。
section组成
代码段( .text)函数定义、程序语句
数据段( .data)初始化的全局变量、初始化的静态局部变量
BSS段( .bss)未初始化的全局变量、未初始化的静态局部变量

比如,在下面的程序中,我们分别定义一个函数、一个全局变量和一个未初始化的全局变量。
//hello.c
intglobal_val=8;
intuninit_val;

voidprint_star(void)
{
   printf("****\n");
}
intmain(void)
{
   print_star();
   return0;
}
接着,我们使用 GCC 编译这个程序,并查看生成的可执行文件 a.out 的符号表和 section header 表信息。



通过符号表和节头表 section header table 信息,我们可以看到,函数 print_star 被放在可执行文件中的 .text section,即代码段;初始化的全局变量 global_val 被放在了 a.out.data section,即数据段;而未初始化的全局变量 uninit_val 则被放在了.bss section,即 BSS 段。

编译器在编译程序时,是以源文件为单位,将一个个源文件编译生成一个个目标文件。在编译过程中,编译器都会按照这个默认规则,将函数、变量分别放在不同的 section 中,最后将各个 section 组成一个目标文件。编译过程结束后,链接器接着会将各个目标文件组装合并、重定位,生成一个可执行文件。

链接器是如何将各个目标文件组装成一个可执行文件的呢?很简单,链接器首先会分别将各个目标文件的代码段整合,组装成一个大的代码段;将各个目标文件中的数据段整合,合并成一个大的数据段;接着将合并后的新代码段、数据段再合并为一个文件;最后经过重定位,就生成了一个可以运行的可执行文件了。

现在又有一个疑问来了,链接器在将各个不同的 section 段组装成一个可执行文件的过程中,各个 section 的顺序如何排放呢?比如代码段、数据段、BSS 段、符号表等,谁放在前面?谁放在后面?

链接器在链接过程中,会将不同的 section,按照链接脚本中指定的各个 section 的排放顺序,组装成一个可执行文件。一般在 Ubuntu 等 PC 版本的系统中,系统会有默认的链接脚本,不需要程序员操心。
$ld--verbose
我们使用上面命令,就可以查看编译当前程序时,链接器使用的默认链接脚本。在嵌入式系统中,因为是交叉编译,所以软件源码一般会自带一个链接脚本。比如在 U-boot 源码的根目录下面,你会看到一个 u-boot.lds 的文件,这个文件就是编译 U-boot 时,链接器要使用的链接脚本。在 Linux 内核中,同样会有 vmlinux.lds 这样一个链接脚本。

属性 section 编程示例


在 GNU C 中,我们可以通过 __attribute__的 section 属性,显式指定一个函数或变量,在编译时放到指定的 section 里面。通过上面的程序我们知道,未初始化的全局变量是放在 .data section 中的,即放在 BSS 段中。现在我们就可以通过 section 属性,把这个未初始化的全局变量放到数据段 .data 中。



通过上面的 readelf 命令查看符号表,我们可以看到,uninit_val 这个未初始化的全局变量,通过__attribute__((section(".data"))) 属性声明,就被编译器放在了数据段 .data section 中。

6.3 U-boot 启动过程中的镜像自拷贝分析


有了 section 这个属性,我们接下来就可以试着分析,U-boot 在启动过程中,是如何将自身代码加载的 RAM 中的。

搞嵌入式的都知道 U-boot,U-boot 的用途主要是加载 Linux 内核镜像到内存、给内核传递启动参数、然后引导 Linux 操作系统启动。

U-boot 一般存储在 Nor flash 或 NAND Flash 上。无论从 Nor Flash 还是从 Nand Flash 启动,U-boot 其本身在启动过程中,也会从 Flash 存储介质上加载自身代码到内存,然后进行重定位,跳到内存 RAM 中去执行。这个功能一般叫做“自举”,是不是感觉很牛 X?U-boot 重定位的过程今天就不展开了,有兴趣的同学,可以看看我的嵌入式视频教程《C 语言嵌入式 Linux 高级编程》第3期:程序的编译、链接和运行。今天我们的主要任务是去看看 U-boot 是怎么完成自拷贝的,或者说它是怎样将自身代码从 Flash 拷贝到内存 RAM 中的。

在拷贝自身代码的过程中,一个主要的疑问就是,U-boot 是如何识别自身代码的?是如何知道从哪里拷贝代码的?是如何知道拷贝到哪里停止的?这个时候我们不得不说起 U-boot 源码中的一个零长度数组。
char__image_copy_start[0] __attribute__((section(".__image_copy_start")));
char__image_copy_end[0] __attribute__((section(".__image_copy_end")));
这两行代码定义在 U-boot-2016.09 中的 arch/arm/lib/section.c 文件中。在其它版本中可能路径不同或者没有定义,为了分析这个功能,建议大家可以下载 U-boot-2016.09 这个版本的U-boot源码。

这两行代码的作用是分别定义一个零长度数组,并告诉编译器要分别放在 .image_copy_start 和 `.image_copy_end` 这两个 section 中。

链接器在链接各个目标文件时,会按照链接脚本里各个 section 的排列顺序,将各个 section 组装成一个可执行文件。U-boot 的链接脚本 u-boot.lds 在 U-boot 源码的根目录下面。



通过链接脚本我们可以看到,__image_copy_start__image_copy_end 这两个 section,在链接的时候分别放在了代码段 .text 的前面、数据段 .data 的后面,作为 U-boot 拷贝自身代码的起始地址和结束地址。而在这两个 section 中,我们除了放2个零长度数组外,并没有再放其它变量。根据前面的学习我们知道,零长度数组是不占用存储空间的,所以上面定义的两个零长度数组,其实就分别代表了 U-boot 镜像要拷贝自身镜像的起始地址和结束地址。
char__image_copy_start[0] __attribute__((section(".__image_copy_start")));
char__image_copy_end[0] __attribute__((section(".__image_copy_end")));
无论 U-boot 自身镜像是存储在 Nor Flash,还是 Nand Flash 上,我们只要知道了这两个地址,就可以直接调用相关代码拷贝。

接着在 arch/arm/lib/relocate.S 中,ENTRY(relocate_code) 汇编代码主要完成代码拷贝的功能。



在这段汇编代码中,寄存器 R1、R2 分别表示要拷贝镜像的起始地址和结束地址,R0 表示要拷贝到 RAM 中的地址,R4 存放的是源地址和目的地址之间的偏移,在后面重定位过程中会用到这个偏移值。
ldr r1, =__image_copy_start
见上面指令,在汇编代码中,ARM的 ldr 指令立即寻址,直接对数组名进行引用,获取要拷贝镜像的首地址,并保存在 R1 寄存器中。数组名本身其实就代表一个地址。通过这种方式,U-boot 在嵌入式启动的初始阶段,就完成了自身代码的拷贝工作:从 Flash 上拷贝自身镜像到 RAM 中,然后再进行重定位,最后跳到 RAM 中执行。

本教程根据《C语言嵌入式Linux高级编程》视频教程第05期改编,更多嵌入式C进阶学习视频教程,可访问王老师淘宝店:wanglitao.taobao.com。本教程电子书可到 QQ群:宅学部落(475504428 )下载。


-------------------------------------------------------------------------
我们尊重原创,也注重分享,如若侵权请联系qter@qter.org。
-------------------------------------------------------------------------

鲜花

握手

雷人

路过

鸡蛋

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了