= 动机
设想,如果你想在程序中支持多种后端(backends),但在开发过程中并不能预计会有多少种。一般来说,会想着抽象出这种后端的描述,然后用动态模块的方式,如 shared library 实现所谓的插件。使用插件,可以实现动态的功能插入,但如果你不想动态的功能插入,所有功能在编译期间就已经确定呢。当然你可以使用一个数组硬编码维护,但这样增删功能就需要改代码,这里介绍一种使用 ld script 的方法。这种方法在 linux kernel 的代码中也使用到了。
= 关于 ld script
即使你不知道, ld script 在每次你链接程序的时候都被使用了,ld程序自带一个默认的脚本,如果你不指定其他的脚本就使用默认的。 ld script 的作用是描述如何组织从目标文件中得到的所有段,最终链接成最终的 elf 输出。
关于 ld script 更多的信息,可以参考 ld 的 info page。
= 定义 ldscript
这里实现一个简单的示例程序,结构如下:主程序遍历一个外部静态数组,打印里面的所有字符串,直到结束;其他目标文件分别向该数组插入一个或多个字符串,而无须修改主程序的代码或者执行额外的启动代码。
file:main.c
[code]
#include
#include
extern const char array_begin;
extern const char array_end;
int main ( void ){
const char *array_iter;
printf( "array_begin : %p , array_end : %p\n", &array_begin, &array_end);
for ( array_iter = &array_begin;
array_iter < &array_end;
array_iter += strlen(array_iter) + 1 ){
printf ( "%3d:%s\n", strlen(array_iter), array_iter );
}
return 0;
}
[/code]
其中 array_begin, array_end 为外部变量,他们会在 ld script 中定义。首先使用 ld --verbose 得到一份 ld 的默认脚本(去掉开头和结尾的多余内容输出),存为 array_iter.ld 文件。 找到 .data 断的定义,修改为类似如下的内容 :
file:array_iter.ld(parts)
[code]
.data :
{
array_begin = .;
*(.extarray)
array_end = .;
*(.data .data.* .gnu.linkonce.d.*)
KEEP (*(.gnu.linkonce.d.*personality*))
SORT(CONSTRUCTORS)
}
[/code]
其中 "*(.extarray)"的意思是,将所有输入目标文件的 .extarray 段输出到这里,并且在周围分别设上两个变量 array_begin, array_end,他们就是在 main.c 中定义的外部变量。他们都被赋予了固定特殊变量".","."的意思是当前地址。
再来看 Makefile 如何写 :
file:Makefile
[code]
OBJS=
array_iter : extarray.h array_iter.ld main.o $(OBJS)
gcc $(CFLAGS) -Wl,-T,array_iter.ld main.o $(OBJS) -o $@
clean :
rm -f *.o array_iter
[/code]
= 添加模块
在上面的 Makefile 中 OBJS 列表为空,我们现在就来添加几个模块 :
file:extarray.h
[code]
typedef const char extarray_t[];
#define extarray extarray_t __attribute__((section(".extarray")))
[/code]
file:1.c
[code]
#include "extarray.h"
extarray a1 = "1";
extarray a2 = "test 1";
extarray a3 = "";
[/code]
file:2.c
[code]
#include "extarray.h"
extarray a4 = "HHIIOOIIYY";
[/code]
修改 Makefile, 添加上 :
OBJS=1.o 2.o
然后执行命令 :
$ make
cc -pipe -Wall -W -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations -g -c -o main.o main.c
cc -pipe -Wall -W -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations -g -c -o 1.o 1.c
cc -pipe -Wall -W -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations -g -c -o 2.o 2.c
gcc -pipe -Wall -W -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations -g -Wl,-T,array_iter.ld main.o 1.o 2.o -o array_iter
$ ./array_iter
array_begin : 0x80496d8 , array_end : 0x80496ed
1:1
6:test 1
0:
10:HHIIOOIIYY
无须修改其他文件,字符串已经被准确输出了。
再来看看内存模型 :
$ objdump -t array_iter| fgrep '.data' | sort -n
080496d8 g .data 00000000 array_begin
080496d8 g O .data 00000002 a1
080496d8 l d .data 00000000 .data
080496da g O .data 00000007 a2
080496e1 g O .data 00000001 a3
080496e2 g O .data 0000000b a4
080496ed g .data 00000000 array_end
080496f0 g .data 00000000 __data_start
080496f0 w .data 00000000 data_start
注意到因为 array_begin, array_end 被赋予了一个常数,所以连接器没有为他们分配空间。
= 现实例子
在 PC x86 中,有多种通用的显示输出方式,常用的如 bios 提供的以字符为基础的输入输出例程,还有就是 vesa 2.0 显卡规范。在 linux x86 启动过程中,kernel 根据配置选择合适的视频输出设备。 实现中, kernel 就用到了 ldscript, 见 :
linux-2.6/arch/x86/boot/setup.ld :
.videocards : {
video_cards = .;
*(.videocards)
video_cards_end = .;
}
linux-2.6/arch/x86/boot/video.h :
...
#define __videocard struct card_info __attribute__((section(".videocards")))
...
extern struct card_info video_cards[], video_cards_end[];
...
阅读(3014) | 评论(0) | 转发(0) |