本文共 3705 字,大约阅读时间需要 12 分钟。
本文将综合以下4篇文章,学习如何写出不依赖libc库的程序:
如果没有看上面4篇文章的,建议先按照顺序学习上述4篇文章,再来看这篇文章,不然有些地方会很突兀。
文章目录
1 本篇文章的目的
那么本篇文章的目的是什么呢?
- 我们想编写一个体积非常小的可执行程序。
- 通过makefile完成代码编译
- 运行后在屏幕上打印“D.T.SoftWare”
但凡是学习过C语言基础语法的人都能写出来这个程序 ,这不就是一个"hello word " 程序么?就像下面这样.
hello.c
#includeint main(){ printf("D.T.SoftWare\n"); return 0;}
我们编译上述hello.c,得到可执行程序hello,可以看出hello的大小是:
竟然有7135字节!!!这其实是因为虽然我们只写了个printf函数,但是实际上在编译链接的过程,链接器将一大堆库函数都与我们的hello程序进行链接,包括一些入口函数啊,进程初始化函数以及printf这个库函数等等,看起来只有一个printf库函数,但是实际上一与libc库进行链接,就会有一大堆东西(这些东西是啥吗,请参看上述四篇文章)。
所以,我们的目的,并不是简单的写出上面的hello.c程序。我们想写一个程序,进行编译链接后,体积达到最小?该如何做到?
我们的分析思路大概如下图:
- 我们知道main函数执行前还有一大堆的初始化函数需要调用,我们不依赖于libc库进行编译链接,以及编写自定义入口函数的链接脚本可以实现
- 不依赖libc库打印字符串的话,就直接进行系统调用,直接调用sys_write函数,而不是调用printf函数
- 直接调用sys_write函数的方法的话就是使用内嵌汇编语言进行调用
学过我上面篇文章的朋友就一定会发现上述的几点,都在那4篇文章中实现过。所以我们可以很轻松的进行代码的编写。
2 解决方案设计
上面已经给出了基本的程序设计思路,现在我们来给出更加具体的程序设计思路。
- 通过内嵌汇编自定义打印函数和退出函数(具体来说就是使用INT 0X80指令)
- 通过编写自定义的链接脚本来指定我们自己的入口函数(不依赖任何库和GCC的任何内置功能)
- 删除可执行程序的无用信息,比如无用的调试信息和段信息。这是通过链接脚本指定的
那么我们先来通过内嵌汇编设计一下打印函数和退出函数,这里要参考。
- 打印函数print:
void print(const char* s, int l){ asm volatile ( "movl $4, %%eax\n" // sys_write系统函数相关 "movl $1, %%ebx\n" "movl %0, %%ecx\n" "movl %1, %%edx\n" "int $0x80 \n" //通过80H进行系统调用 : : "r"(s), "r"(l) // print的参数 : "eax", "ebx", "ecx", "edx" // 保留列表 );}
上述打印函数在这篇文章中一讲非常详细。
- 退出函数 exit
void exit(int code){ asm volatile ( "movl $1, %%eax\n" //sys_exit "movl %0, %%ebx\n" "int $0x80 \n" : : "r"(code) //参数 : "eax", "ebx" );}
上述退出函数也在这篇文章中一讲非常详细。
- 链接脚本设计 program.lds
上述链接脚本中已经注解的非常详细,当然,还是需要参考这篇文章先学习以下链接脚本的语法与概念最好。
那么上述程序设计基本上完成,下面我们给出完成的代码:
- 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" "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" );}
- program.lds 链接脚本
ENTRY(program)SECTIONS{ .text 0x08048000 + SIZEOF_HEADERS : { *(.text) *(.rodata) } /DISCARD/ : { *(*) }}
- 当然,为了编译方便,我还给出了makefile文件,方便我们程序的编译:
CC := gccLD := ldRM := rm -frTARGET := program.outSRC := $(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 allall : $(TARGET)clean : $(RM) $(TARGET) $(OBJ)
上述makefile可能大多数人看不懂,这个无所谓,它只是一种类似于脚本语言的语言,辅助我们编译程序的,我们将上述三个文件:makefile program.lds program.c这三个文件放到linux系统下的同一个目录下,然后输入命令make即可完成代码的编译,生成可执行文件。
- 就像下面这样,我们队我们的程序进行编译
- 运行可执行程序 ./program.out
很明显,我们实现了我们最开始的功能。
-
我们看看我们写的这个可执行程序的大小:
-
上面的是hello的大小,下面的是我们自己的可执行程序的大小,只有582字节,远远小于hello的大小。这正是我们所希望看到的。
3 拓展
如果有详细看上述的makefile文件,我们会发现,在我们编译我们的源文件的时候,使用了静态链接。现在在这里介绍一些链接时的一些选项:
-
ld 命令
- GNU的链接器,将目标文件链接为可执行文件
-
ld -static
- 表示ld使用静态链接的方式来产生最终的可执行程序,而不是默认的动态链接。至于什么是静态链接什么是动态链接,后面肯定会有文章详细学习。
-
gcc -fno-builtin
- -fno-builtin 用于关闭GCC内部函数功能
-
GCC提供了很多内置函数(Built-in Function),它会把一些常用的C库函数替换成编译器内置的函数,以达到优化程序的目的。在上述我们的makefile中就用到了这个选项,以防止编译器的优化
4、总结
-
对于资源受限制的嵌入式设备,需要考虑可执行程序的大小(当然目前各种设备内存都足够大,不必担心这一点)
-
通过在C/C++语言中内嵌汇编语言,可以避免使用库函数而直接使用系统函数
-
可以通过如下方式,来控制可执行程序的大小
- 最小化库的使用(必要的时候,可以自己实现相关函数)
- 自定义链接脚本,删除无用的段信息
本文参考狄泰软件学院相关课程
想学习的可以加狄泰软件学院群, 群聊号码:199546072学习探讨加个人(可以免费帮忙下载CSDN资源):
qq:1126137994 微信:liu1126137994转载地址:https://lyy-0217.blog.csdn.net/article/details/85127538 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!