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

6.1 GNU C 的扩展关键字:attribute

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

attribute 的使用非常简单,当我们定义一个函数、变量或类型时,直接在它们名字旁边添加下面的属性声明即可:

__atttribute__((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字节地址对齐,参数也要使用小括号括起来。如果属性的参数是一个字符串,小括号里的参数还要用双引号引起来。

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

char c2 __attribute__((packed,aligned(4)));
char c2 __attribute__((packed,aligned(4))) = 4;
__attribute__((packed,aligned(4))) char c2 = 4;

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

char c2 = 4 __attribute__((packed,aligned(4)));

6.2 属性声明:section

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

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

程序的编译、链接过程

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

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

$ gcc -o a.out hello.c
$ readelf -S a.out

  here are 31 section headers, starting at offset 0x1848:
Section Headers:
  [Nr] Name              Type            Addr     Off    Size
  [ 0]                   NULL            00000000 000000 000000
  [ 1] .interp           PROGBITS        08048154 000154 000013
  [ 2] .note.ABI-tag     NOTE            08048168 000168 000020
  [ 3] .note.gnu.build-i NOTE            08048188 000188 000024
  [ 4] .gnu.hash         GNU_HASH        080481ac 0001ac 000020
  [ 5] .dynsym           DYNSYM          080481cc 0001cc 000040
  [ 6] .dynstr           STRTAB          0804820c 00020c 000045
  [ 7] .gnu.version      VERSYM          08048252 000252 000008
  [ 8] .gnu.version_r    VERNEED         0804825c 00025c 000020
  [ 9] .rel.dyn          REL             0804827c 00027c 000008
  [10] .rel.plt          REL             08048284 000284 000008
  [11] .init             PROGBITS        0804828c 00028c 000023
  [13] .plt.got          PROGBITS        080482d0 0002d0 000008
  [14] .text             PROGBITS        080482e0 0002e0 000172
  [15] .fini             PROGBITS        08048454 000454 000014
  [16] .rodata           PROGBITS        08048468 000468 000008
  [17] .eh_frame_hdr     PROGBITS        08048470 000470 00002c
  [18] .eh_frame         PROGBITS        0804849c 00049c 0000c0
  [19] .init_array       INIT_ARRAY      08049f08 000f08 000004
  [20] .fini_array       FINI_ARRAY      08049f0c 000f0c 000004
  [21] .jcr              PROGBITS        08049f10 000f10 000004
  [22] .dynamic          DYNAMIC         08049f14 000f14 0000e8
  [23] .got              PROGBITS        08049ffc 000ffc 000004
  [24] .got.plt          PROGBITS        0804a000 001000 000010
  [25] .data             PROGBITS        0804a020 001020 00004c
  [26] .bss              NOBITS          0804a06c 00106c 000004
  [27] .comment          PROGBITS        00000000 00106c 000034
  [28] .shstrtab         STRTAB          00000000 00173d 00010a
  [29] .symtab           SYMTAB          00000000 0010a0 000470
  [30] .strtab           STRTAB          00000000 001510 00022d

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

我们知道一段源程序代码在编译生成可执行文件的过程中,函数和变量是放在不同段中的。一般默认的规则如下。

section 组成
代码段( .text) 函数定义、程序语句
数据段( .data) 初始化的全局变量、初始化的静态局部变量
BSS段( .bss) 未初始化的全局变量、未初始化的静态局部变量

比如,在下面的程序中,我们分别定义一个函数、一个全局变量和一个未初始化的全局变量。

//hello.c
int global_val = 8;
int uninit_val;

void print_star(void)
{
    printf("****\n");
}
int main(void)
{
    print_star();
    return 0;
}

接着,我们使用 GCC 编译这个程序,并查看生成的可执行文件 a.out 的符号表和 section header 表信息。

$ gcc -o a.out hello.c
$ readelf -s a.out
$ readelf -S a.out
符号表信息:
Num:  Value   Size Type    Bind   Vis      Ndx Name
37: 00000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
48: 0804a024     4 OBJECT  GLOBAL DEFAULT   26 uninit_val
51: 0804a014     0 NOTYPE  WEAK   DEFAULT   25 data_start
52: 0804a020     0 NOTYPE  GLOBAL DEFAULT   25 _edata
53: 080484b4     0 FUNC    GLOBAL DEFAULT   15 _fini
54: 0804a01c     4 OBJECT  GLOBAL DEFAULT   25 global_val
55: 0804a014     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
61: 08048450    93 FUNC    GLOBAL DEFAULT   14 __libc_csu_init
62: 0804a028     0 NOTYPE  GLOBAL DEFAULT   26 _end
63: 08048310     0 FUNC    GLOBAL DEFAULT   14 _start
64: 080484c8     4 OBJECT  GLOBAL DEFAULT   16 _fp_hw
65: 0804840b    25 FUNC    GLOBAL DEFAULT   14 print_star
66: 0804a020     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
67: 08048424    36 FUNC    GLOBAL DEFAULT   14 main
71: 080482a8     0 FUNC    GLOBAL DEFAULT   11 _init
section header信息:
Section Headers:
  [Nr] Name         Type        Addr     Off    Size
  [14] .text        PROGBITS    08048310 000310 0001a2
  [25] .data        PROGBITS    0804a014 001014 00000c
  [26] .bss         NOBITS      0804a020 001020 000008
  [27] .comment     PROGBITS    00000000 001020 000034
  [28] .shstrtab    STRTAB      00000000 001722 00010a
  [29] .symtab      SYMTAB      00000000 001054 000480
  [30] .strtab      STRTAB      00000000 0014d4 00024e

通过符号表和节头表 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 中。

int global_val = 8;
int uninit_val __attribute__((section(".data")));
int main(void)
{
    return 0;
}

通过上面的 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源码。

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

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

OUTPUT_FORMAT("elf32-littlearm",
    "elf32-littlearm",
    "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
 . = 0x00000000;
 . = ALIGN(4);
 .text :
 {
  *(.__image_copy_start)
  *(.vectors)
  arch/arm/cpu/armv7/start.o (.text*)
  *(.text*)
 }
 . = ALIGN(4);
 .data : {
  *(.data*)
 }
    ...
    ...
 . = ALIGN(4);
 .image_copy_end :
 {
  *(.__image_copy_end)
 }
 .end :
 {
  *(.__end)
 }
 _image_binary_end = .;
 . = ALIGN(4096);
 .mmutable : {
  *(.mmutable)
 }
 .bss_start __rel_dyn_start (OVERLAY) : {
  KEEP(*(.__bss_start));
  __bss_base = .;
 }
 .bss __bss_base (OVERLAY) : {
  *(.bss*)
   . = ALIGN(4);
   __bss_limit = .;
 }
 .bss_end __bss_limit (OVERLAY) : {
  KEEP(*(.__bss_end));
 }
}

通过链接脚本我们可以看到,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) 汇编代码主要完成代码拷贝的功能。

ENTRY(relocate_code)
    ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */
    subs    r4, r0, r1      /* r4 <- relocation offset */
    beq relocate_done       /* skip relocation */
    ldr r2, =__image_copy_end   /* r2 <- SRC &__image_copy_end */

copy_loop:
    ldmia   r1!, {r10-r11}      /* copy from source address [r1]    */
    stmia   r0!, {r10-r11}      /* copy to   target address [r0]    */
    cmp r1, r2          /* until source end address [r2]    */
    blo copy_loop

在这段汇编代码中,寄存器 R1、R2 分别表示要拷贝镜像的起始地址和结束地址,R0 表示要拷贝到 RAM 中的地址,R4 存放的是源地址和目的地址之间的偏移,在后面重定位过程中会用到这个偏移值。

ldr r1, =__image_copy_start

见上面指令,在汇编代码中,ARM的 ldr 指令立即寻址,直接对数组名进行引用,获取要拷贝镜像的首地址,并保存在 R1 寄存器中。数组名本身其实就代表一个地址。通过这种方式,U-boot 在嵌入式启动的初始阶段,就完成了自身代码的拷贝工作:从 Flash 上拷贝自身镜像到 RAM 中,然后再进行重定位,最后跳到 RAM 中执行。

本教程根据 C语言嵌入式Linux高级编程视频教程 第05期 改编,电子版书籍可加入QQ群:475504428 下载,更多嵌入式视频教程,可关注:
微信公众号:宅学部落(armlinuxfun)
51CTO学院-王利涛老师:http://edu.51cto.com/sd/d344f

原文地址:http://blog.51cto.com/zhaixue/2348616

时间: 02-02

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

后ARM时代,嵌入式工程师的自我修养

1嵌入式学习的一些概念理解误区 很多嵌入式初学者认为,学嵌入式,就是学习ARM,就是学习开发板.买一块开发板,然后在上面"移植"u-boot.Linux内核,再使用busybox制作一个根文件系统,大功告成!觉得可以出去找工作了.这其实是有一定片面性的:首先ARM是个CPU架构,跟PC上的X86架构一样,你见过有人在Windows下面学习C/C++编程.MFC编程.网络编程.互联网编程,说自己学习X86的吗?当然,也不可否认,嵌入式平台的多样性.硬件的可定制性导致我们在嵌入式平台上开发

一个程序员的自我修养

在网上看到一篇程序员的自我修养,深以为然,不禁摘录一些,勉励自己 一个好的开发人员,应该能够全面.高效.严谨的去处理任何软件程序和业务问题,成为一个好的开发,是一个很有意思的话题,不过无论这个话题如何开展,基础两个字必不可少,虽然代码量是衡量开发能力的重要指标,但仅能够熟练的进行代码编写是不够的,更要能深刻的理解技术原理和业务逻辑,扎实的个人基础和技术基础往往会促进代码的编写,更游刃有余的解决问题. 下面说的一些基础,可能绝大部分开发人员都不会在意甚至忽略,但恰恰这些才是开发大厦的基石. 1.科

程序员的自我修养:(1)目标文件

程序员的自我修养:(1)目标文件 1.目标文件 1.1 编译与链接 在使用像Visual Studio或Qt Creator等IDE时,通常有一个叫做"构建"的按钮.当编辑完成要运行和测试时点一下它,程序就能跑起来了,所以我们很少关心编译和链接.其实,编译和链接合并在一起就称为 构建(Build).简单的一次按键,实际背后却是异常复杂的过程: 预编译(Preprocessing) 编译(Compilation) 扫描:算法类似有限状态机(FSM),将字符转换成Token. 语法分析:分

一个前端的自我修养

①一个前端的自我修养 今天给大家分享的主题是前端的自我成长,这是一个关于成长的话题. 很多人都有这样的感觉:听了很多技术圈子的分享,有的有深度,有的循循善诱,深入浅出,但是呢,几年下来,到底哪些用上了,哪些对自己真的有帮助了?反而有些模糊. 2015 年我在不同的场合分享了很多内容:有移动端的性能.有适配.有 Web vs Native,也有 hybrid,但是其实我一直比较担心,真正有深度的内容,其实面向的是比较小众的群体,比如说 Hybrid,其实它在大部分公司里面,是只能用现成的. 所以我

程序员的自我修养(一)

程序员的自我修养,最开始看这本书是在学校的图书馆,当时翻了几下,发现这本书内容还真挺特别的.是浙大几个老师写的,就更感觉亲切了,所以自己买了一本书来看看,这也是我到研究生之后买的第一本书了,哈哈,平时都是pdf啥的,其实pdf真不好看. 言归真正,讲讲昨天看到的 从一个HelloWorld程序开始讲起 #include<stdio.h> int main() { printf("helloworld") return 0; } 从高级语言到操作系统可以执行的语言,这里有几个

读《程序员的自我修养》感受

这书不错,链接-装载-库 我觉得是很底层的东西.比如很多人闭着眼睛都能写出来的hello world(当然不包括brianfuck,如果你会,你真的闹残了吗= =), 其实链接编译器做了很多,不然就哪来的printf,这IO初始化也是CRT(c runtime)库完成的.堆栈的初始化,还有系统装载让程序运行等等.涉及很多. 书里后面就讲了一个CRT库,自己写一个,感觉不错,学了很多.比如malloc,free的实现,话说还是跨平台的.当然库很小,功能不多,不过写这个也可以学学算法.内存的分配,这

《程序员的自我修养》 第二章——编译和链接

摘自http://blog.chinaunix.net/uid-26548237-id-3839979.html <程序员的自我修养>第二章——编译和链接 2.1 被隐藏了的过程    C语句的经典,“Hello World”程序几乎是每个程序员闭着眼睛都能写出的,编译运行一气呵成,基本成了程序入门和开发环境测试的默认标准. #include <stdio.h> int main() { printf("Hello World\n"); return 0; 在L

读书笔记:程序员的自我修养-----第一章(综述)

题前:30--45天读完,一周至少3篇读书笔记.不能坚持,不再联系,不再找你. 一. hello world 程序引出的问题,看40天后,再回来看看自己的答案,提升多少. Q1:程序为什么要被编译器编译之后才可以运行?   A1 : 系统执行的机器语言,即二进制文件,程序是文本文件需要编译之后,由链接器链接需要的基本库生成二进制文件. Q2: 编译器在把C语言程序转换成可以执行的机器码的过程中作了什么,怎么做的?   A2: 预处理,汇编器生成汇编文件,编译器生成目标文件,链接器链接生成可执行文

很认真的聊一聊程序员的自我修养

首先要谈的是,今天的话题所聊的程序员包含哪些人? 在中国,写程序,不仅仅是一种兴趣,更多的时候,还是一种普通职业和谋生工具 大公司有厉害的程序员,优秀的架构师,但大量的小公司也有很多普通的程序员.在我这些年的工作经历中,也越来越深刻的感受到普通程序员的影响和力量.对于高阶程序员,所谓八仙过海各有神通,各有各的成就,各有各的修养,但程序员在达成较高的水平之前,有一些"自我修养",是最基础的,是普世的. 所以今天的话题面向的程序员,就是所有的正在写代码或者曾经写过代码的程序员,也包括广义上

嵌入式C语言全套视频教程云盘下载!

了解过嵌入式开发的朋友们都有一定的了解,语言是学习嵌入式开发必须具备的工具语言,学好C语言基础可以更好的学习嵌入式开发.今天在这里给大家分享一个嵌入式C语言全套视频教程,需要的朋友可以下载来看看! 课程目录部分截图: 百度云盘下载:http://pan.baidu.com/s/1c1OETIo 密码:cgcj