在上节博客中,我们介绍了链接器的相关概念。那么在本节,我们就继续来看看链接器,看看它在工程实践中的应用。我们在本节中做一个实验,来模拟在嵌入式中的开发。
实验的目标:
1、编写一个“体积受限”的可执行程序;
2、通过 makefile 完成代码编译;
3、运行后在屏幕上打印“D.T.Software”
那么这个目标看似很简单,我们在初学编程时就直接打印经典的 hello world。其效果类似,经典的代码是
#includeint main() { printf("D.T.Software"); return 0; }
当然这个代码也能运行,但是我们的要求是体积受限。什么是体积受限呢?我们都知道,在嵌入式的开发中,内存是很少的。因此节省内存成立必不可少的因素,因此我们必须要去掉库文件所包含的一些额外的信息,只保留打印语句所需要代码即可。那么我们来深度分析下,如何进行开发呢?无疑是直接用汇编代码进行编写,然后编写链接脚本自定义入口地址了。思路如下
由上可知,我们的解决方案可由以下几部分完成:
1、通过内嵌汇编自定义打印函数和退出函数(INT 80H);
2、通过链接脚本自定义入口函数(不依赖于任何库和 GCC 内置功能);
3、删除可执行程序中的无用信息(无用段信息,调试信息等)。
打印函数设计如下
void print(const char* s, int l) { asm volatile ( "movl $4, %%eax\n" // sysy_write "movl $1, %%ebx\n" "movl %0, %%ecx\n" "movl %1, %%edx\n" "int $0x80 \n" // 80H Service : : "r"(s), "r"(1) // parameter : "eax", "ebx", "ecx", "edx"); }
退出函数设计如下
void exit(int code) { asm volatile ( "movl $1, %%eax\n" // sys_exit "movl %0, %%ebx\n" "int $0x80 \n" // 80H Service : : "r"(code) // parameter : "eax", "ebx"); }
链接脚本设计如下
ENTRY(program) // 指定入口函数 SECTIONS { .text 0x08048000 + SIZEOF_HEADRS : { *(.text) // 将目标文件中的这两个段合并进入到可执行程序中 *(.rodata) } /DISCARD/ : { *(*) // 放弃所有目标文件中除 .test 和 .rodata 之外的其他段 } }
最后来介绍几个命令:
1、ld 命令:GNU 的链接器,将目标文件链接为可执行程序;GCC 编译器集中的一员。
2、ld -static:表示 ld 使用静态链接方式来产生最终程序,而不是采用默认的动态链接方式。
3、gcc -fno-builtion:用于关闭 GCC 内置函数的功能。
在 GCC 中,它提供了很多内置函数(Built-in Function),-fno-builtion 选项会把一些常用的 C 库函数替换成编译器的内置函数,以达到优化的目的。
program.c 源码
void print(const char* s, int l); void exit(int code); void program() { print("D.T.Software\n", 13); exit(0); } void print(const char* s, int l) { asm volatile ( "movl $4, %%eax\n" // 调用系统写函数,编号为 4 "movl $1, %%ebx\n" // 指定参数,将字符串打印到屏幕 "movl %0, %%ecx\n" // 占位符,代表第一个参数(地址) "movl %1, %%edx\n" "int $0x80 \n" // 开中断 : // 输入参数为空 : "r"(s), "r"(l) // 以只读形式传入参数 : "eax", "ebx", "ecx", "edx" // 初始化寄存器 ); } void exit(int code) { asm volatile ( "movl $1, %%eax\n" "movl %0, %%ebx\n" "int $0x80 \n" : : "r"(code) : "eax", "ebx" ); }
makefile 编写如下
CC := gcc LD := ld RM := rm -rf TARGET := program.out SRC := $(TARGET:.out=.c) OBJ := $(TARGET:.out=.o) LDS := $(TARGET:.out=.lds) .PHONY : rebuild clean all $(TARGET) : $(OBJ) $(LDS) $(LD) -static -T $(LDS) -o $@ $< @echo "Target File ==> $@" $(OBJ) : $(SRC) $(CC) -fno-builtin -o $@ -c $^ rebuild : clean all all : $(TARGET) clean : $(RM) $(TARGET) $(OBJ)
我们来运行看看结果如何呢?
结果正是我们想要的,那么我们这么大费周章的来写这么复杂的代码,究竟意义何在呢?我们来看看它的内存大小,以及来编写一个正常的 printf 打印语句的代码,对比下两个可执行文件的内存大小
我们看到效果是一样的,但是正常的 printf 打印和我们自定义的链接脚本的函数编写所需的内存空间是十多倍的差异。也就是说,在嵌入式开发中,资源是很宝贵的这种做法是必须的。通过今天对链接脚本的实例分析,总结如下:1、对于资源受限的嵌入式设备,需要考虑可执行程序的文件大小;2、通过内嵌汇编直接使用系统服务能够避开相关库的使用;3、可以通过如下方法控制可执行程序的体积大小:a> 最小化库的使用(必要情况下自己实现相关函数);b> 自定义链接脚本,删除无用段信息。