链接脚本在编程中的高级运用之一:可变长数组

作为嵌入式软件工程师,应该要清楚程序的每一条指令在哪里,什么时候会被加载到内存,什么时候会被执行。链接脚本会明确告诉你程序的代码和数据在内存中的分布。精确控制代码和数据在内存中的分布是高效利用内存资源的前提。自定义链接脚本是资深嵌入式软件工程师的必备技能,更是嵌入式架构师的最基本要求。此外,灵活定制链接脚本在编程方面有更高级的应用。

一、编译链接原理

简单讲述编译链接的基本原理有助于后面内容的理解。

a. 简单点说,一个可执行程序包括文件头、代码段(.text)、数据段(.bss)、符号段等信息,在Linux GCC工具链中,该执行程序文件的格式是elf,而在elf格式中,段称为section。现加上某个程序有file1.c和file2.c两个文件。

b. 一个file1.c文件中有代码(函数),也有数据(全局变量,假设都没有初始化的),因此在编译(arm-linux-gcc)之后就会产生一个对应的file1.o,在该文件中会有产生.text section,其会将file.c中所有的函数代码编译后的指令放到该区域,假设长度是0x100字节;同样会产生.bss section,会将所有的全局变量定义到该区域,假设长度是0x20字节。这时,file1.o是可重定位的文件,即.text段是从地址0开始的,其真正的虚拟运行地址还需要在链接阶段重定位完成。

c. 对于另一个file2.c文件,同样会产生对应的file2.o,而该文件中会有对应的.text section(假设长度是0x200字节)和.bss section(假设长度是0x40字节)。其.text段同样是从地址0开始。

d. 链接的最终结果就是产生唯一的一个可执行文件result,而该文件只有一个.text段和一个.bss段,而且.text段会按照指定的链接地址重定位好,即.text段不再是从0开始。那么从这个结果来看,链接的过程应该包括这两个步骤:

d1. 合并file1.o的.text和file2.o的.text到result的.text,其长度为0x100+0x200=0x300字节;合并file1.o的.bss和file2.o的.bss到result的.bss,长度是0x20+0x40=0x60字节

d2. 根据指定的链接地址重定位result的.text段和.bss段,即.text段从指定的地址开始,各个函数的起始地址也会根据该地址进行重新定位。

二、链接脚本基本语法

这里以最基础的链接脚本语法作为示例,GCC工具链下的链接脚本后缀一般是.lds。

ENTRY(_start)//指定_start为程序的第一条指令

SECTIONS//指定内存分布

{

//顿号是地址标识符,这里指定为0x40000000是虚拟运行地址

. = 0x40000000;

//即.text的地址是从0x40000000开始,.text的内容包括file1.o和file2.o的.text

.text :

{

file1.o (.text)//先合并file1.o的.text

file2.o (.text) //再合并file2.o的.text

}

//此时地址是:0x40000000+0x300=0x40000300

//地址标识符可以进行表达式计算,

//则.bss的起始地址是0x40000300+0x400=0x40000700

. = . + 0x400;

.bss:

{

file1.o(.bss)

file2.o(.bss)

}

}

三、关键的链接命令参数

链接命令程序是:arm-linux-ld,通过在命令行传入-T参数来指定自定义的链接脚本。如–Tfile.lds 代表链接时使用自定义的file.lds脚本,而不是系统默认的链接脚本。

四、可变长数组的实现

介绍完编译链接原理,我们开始讲述链接脚本在编程中的第一个高级运用--可变长数组。可变数组我们一般见于C++或者JAVA等高级语言,其是指在运行过程中动态地改变数组的大小,往往是通过链表队列的方式来实现变长需求。我们这里所阐述的可变长数组严格意义上是指静态可变长数组,或者更严格一点讲,在静态链接时数组大小也已经固定,只是在编程形式上看起来是可变长的。

我们在下面将要介绍的uboot启动引导模块中有一个命令模式,即uboot会响应用户输入的命令以达到修改系统参数、显示板级系统信息、加载指定操作系统等目的。Uboot原则上可以支持任意多的命令,只要编程人员愿意添加。我们很容易想到,uboot是根据用户在命令行中输入的命令字符串在事先设置好的命令字符串数组中匹配目标命令的,也很容易想到这个匹配的过程就是for循环中strcmp的过程。既然是for循环,那肯定有一个大小的问题,就是这个数组的大小到底是多少。我们很有可能这样实现:

Struct cmd_type cmd[]={cmd1,cmd2,cmd3,…};

命令个数=sizeof(cmd)/sizeof(cmd_type)

这样的做法是可行的,但是它不够灵活,至少每次添加一条命令都要到该数组中去签到,如果真支持100个命令,那估计得用一个专门的文件来定义这些命令的初始化了。有没有一种比较优雅的方法去灵活支持命令的扩展呢?软件其实也是一种艺术,我们来看看uboot是怎么做的!

1. uboot命令的格式

-include/command.h

struct cmd_tbl_s {

char *name; //命令名称

int maxargs; //最多有几个参数

int repeatable;//是否支持回车即重复命令

int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);//执行命令

char *usage;//命令使用示例

char *help; //命令使用帮助信息

};

typedef struct cmd_tbl_s cmd_tbl_t;

2. 非常重要的宏定义

-include/command.h

#define Struct_Section __attribute__ ((section (".u_boot_cmd")))

#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \

cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}

3. 如何定义一条命令

-Common/command.c

U_BOOT_CMD(

version, 1, 1, do_version,

"version - print monitor version\n",

NULL

);

用户在命令行输入version,那uboot就会调用do_version这个函数来打印uboot的版本。看看它宏展开之后是啥样的:

struct cmd_tbl_s __u_boot_cmd_version __attribute__ ((section(".u_boot_cmd"))) = \

{ version, 1, 1, do_version, "version - print monitor version\n", NULL};

这里的__attribute__ ((section(".u_boot_cmd")))是啥来的,嗯嗯,说了这么多,终于跟链接脚本扯上关系了。__attribute__属性是GCC工具链支持的特性语法,其表示该结构变量在编译后会存放到.u_boot_cmd这个section,这个section是自定义的,而像.text存放代码,.bss存在数据是默认产生的section.
好,咱们的链接脚本该出场了吧。

4. uboot链接脚本

SECTIONS

{…

__u_boot_cmd_start = .;

.u_boot_cmd : { *(.u_boot_cmd) }

__u_boot_cmd_end = .;

…}

-include/command.h中有以下定义:

extern cmd_tbl_t __u_boot_cmd_start;

extern cmd_tbl_t __u_boot_cmd_end;

即__u_boot_cmd_start代表命令数组的开始,而__u_boot_cmd_end代表命令数组的结束。.u_boot_cmd : { *(.u_boot_cmd) }中*代表所有.o文件的.u_boot_cmd内容都合并到可执行文件的.u_boot_cmd中,即所有文件中通过U_BOOT_CMD宏定义的命令都会被合并到该section.

5. 重新看看for循环怎么找到命令的

-common/command.c

就不解释了吧。只说一点,cmdtp是cmd_tbl_t型指针,cmdtp++代表指向下一个cmd_tbl_t结构体,即下一条uboot命令。

六、Linux呢?

有人会问,Linux是否也应用了该特性呢?废话,Linux应该是将C语言的编程技巧运用到登峰造极的地步了,怎么会放过这个武器呢。自己学着总结一下。

还是随便点一下,每个Linux驱动模块的入口是什么啊,记起来不?是

module_init(XXX_init)

看看人家的宏展开,没晕吧~~

既然有链接脚本在编程中的高级运用之一,就会有之二、之三。我们接下来会讲述怎么利用链接脚本来做内存的分时复用的,还会讲讲C++编译器怎么支持类对象的构造和析构函数的。期待吧~~

嵌入式交流群:149294942(QQ)

blog: http://blog.csdn.net/yueqian_scut

我们追求:

1.忠于Linux源码,百分百原创。

2.从上电第一行代码、系统第一行代码、模块第一行代码、应用第一行代码,深入讲解嵌入式软件生命周期。

3 深刻理解硬件体系, 聚焦软件层次设计、模块设计和框架设计。

请关注我们,谢谢!

时间: 2024-10-12 22:17:28

链接脚本在编程中的高级运用之一:可变长数组的相关文章

链接脚本在编程中的高级运用之二——运行时库和C++特性支持

我们在链接脚本在编程中的高级运用之一可变长数组中已经讲述了编译链接的原理,并且以uboot命令为例详细介绍链接脚本如何实现可变长数组.本章在前者的基础上继续讲述链接脚本在运行时库中的高级应用技巧,以及编译器如何支持类对象的构造和析构函数.本章的应用原则上类似于可变长数组,但本章更加侧重讲述运行时库的实现原理,其不仅通过链接脚本的section来实现可变长数组去支持任意多类对象的构造函数和析构函数,而且还支持特定函数体的"可变长". 一.运行时库和类对象的构造.析构函数 很多程序员以为程

链接脚本在编程中的高级运用之二——执行时库和C++特性支持

我们在链接脚本在编程中的高级运用之中的一个可变长数组中已经讲述了编译链接的原理,并且以uboot命令为例具体介绍链接脚本怎样实现可变长数组. 本章在前者的基础上继续讲述链接脚本在执行时库中的高级应用技巧.以及编译器怎样支持类对象的构造和析构函数.本章的应用原则上类似于可变长数组,但本章更加側重讲述执行时库的实现原理,其不仅通过链接脚本的section来实现可变长数组去支持随意多类对象的构造函数和析构函数,并且还支持特定函数体的"可变长". 一.执行时库和类对象的构造.析构函数 非常多程

Python中函数的参数传递与可变长参数

1.Python中也有像C++一样的默认缺省函数 1 def foo(text,num=0): 2 print text,num 3 4 foo("asd") #asd 0 5 foo("def",100) #def 100 定义有默认参数的函数时,这些默认值参数位置必须都在非默认值参数后面. 调用时提供默认值参数值时,使用提供的值,否则使用默认值. 2.Python可以根据参数名传参数 1 def foo(ip,port): 2 print "%s:%d

C++ 11可变参数接口设计在模板编程中应用的一点点总结

概述 本人对模板编程的应用并非很深,若要用一句话总结我个人对模板编程的理解,我想说的是:模板编程是对类定义的弱化. 如何理解“类定义的弱化”? 一个完整的类有如下几部分组成: 类的名称: 类的成员变量(或属性,C#中属性和成员变量还是有区别的): 类的成员方法: 从编译器的角度看,我们必须明确指定以上3部分,才算完整地定义了一个类并且编译通过. 所谓的“类弱化”,是指类的设计者在定义类的时候,并没有完整定义一个类,而是把类的其中一部分的定义留给类的使用者. 从传统才c++98看,通过模板类,使用

关于链接脚本中程序入口的一些问题

http://www.jb51.net/article/62360.htm 在编写普通的c语言程序时,我们都会先写一个main函数,并认为main函数是所有程序的入口函数,其实main函数只是我们所编写的程序的入口函数,真正可执行文件的入口点并不是main函数,在执行main函数之前还有许多的初始化工作需要做,这些在main函数之前的工作是由标准 C 库完成的,然后再由标准  C 库调用main函数. 真正可执行文件的入口点可以通过查看链接脚本(在使用ld命令时加上-verbose参数)可以看出

unix高级编程中的一个头文件 apue.h 与一个差错文件error.c 的内容

在查看unix高级编程中的代码时,如果我们编写书中的代码,发现一般都会报错,这是因为作者在写这本书时,他自己编写了一个头文件,跟一个差错处理文件,出来处理他自己的代码错误信息: 下面我们来看下代码的内容: 我实现第一个代码,关于文件的打开,实现 ls 命令的代码: #include "apue.h"#include <stdio.h>#include <dirent.h> int main(int argc, char *argv[]){ DIR *dp; st

在Bash shell脚本编程中,如何正确无误获取到“脚本选项参数”和“脚本参数”呢?

Linnux 中有些命令的功能非常强大,主要是因为它支持的命令选项比较多.如:[ip]命令可以配置IP地址.路由条目的配置管理操作非常完善,该命令就可以完成[ifconfig]和[route]命令实现的所有功能.函数是单独的功能模块,如果函数能够接收选项参数,那么该函数的功能就变得丰富,且灵活.脚本也是一样. 那么linux中命令的格式又是怎么样的呢? [[email protected] ~]# command   [optons parameter1 | parameter2]...  pa

链接脚本之LMA VMA解释

链接脚本中的LMA和VMA是什么意思.这个问题纠结了一段时间,今天在看<ARM体系结构与编程>时,豁然开朗,写下自己的认识.分享例如以下: LMA:载入地址 位于存储器中的地址  LOAD MEMORY ADDRESS VMA:执行地址(虚拟地址) 执行时的地址 VIRTUAL MEMORY ADDRESS  为什么用VMA表示呐?由于cpu执行的地址都是虚拟地址,经过MMU转为物理地址.在没有开MMU的裸板下,延续了这一称呼.理解为执行地址. 为什么要分 两种地址? 执行映像文件时,有些域能

译:DOM2中的高级事件处理(转)

17.2. DOM2中的高级事件处理(Advanced Event Handling with DOM Level 2)        译自:JavaScript: The Definitive Guide, 5th Edition ----By David Flanagan         迄今为止,在本章中出现的事件处理技术都是DOM0级的一部分,所有支持JavaScript的浏览器都支持DOM0的API.DOM2定义了高级的事件处理API,和DOM0的API相比,有着令人瞩目的不同(而且功