Author:Once Day Date:2023年3月11日
漫漫长路,才刚刚开始…
在Linux下开发多源文件的C代码文件,是一定要了解Makefile的,虽然现在构建工具很多,但学习的一开始,不必追求最新的工具。宜厚积薄发,切勿好高骛远!
首先想象有三个源文件,需要把它们编译成一个源文件,如下:
main.c
,主要执行逻辑。tool.c
,一些辅助工具。debug.c
,一些调试工具。可以如下编译它们:
gcc main.c tool.c debug.c -o main.out
但如果修改了其中的main.c
文件,又需要重新编译一次。
不过仅仅只是修改了main.c
文件,可以不需要全部重新编译,如下:
gcc -c main.c -o main.o
gcc -c tool.c -o tool.o
gcc -c debug.c -o debug.o
gcc main.o tool.o debug.o -o main.out
在修改了main.c
之后,只需要执行以下步骤:
gcc -c main.c -o main.o
gcc main.o tool.o debug.o -o main.out
很明显,未修改的文件无需重新编译,C语言是基于文件编译的,这种方式即增量式编译,对于大项目而言(数百上千的源文件),可节约非常多的时间。
对于上面的过程,对于修改文件的追踪和指令的输入,能够用模板化的文件指定,即Makefile文件。
(Make和Makefile基础语法参考文档:Makefile+Make基础知识_Once_day的博客-CSDN博客)
main: main.o tool.o debug.o
gcc -o main main.o tool.o debug.o #缩进一定要使用tab,不能用空格代替
main.o: main.c
gcc -c main.c
tool.o: tool.c
gcc -c tool.c
debug.o: debug.c
gcc -c debug.c
为什么依赖关系写得这么多?因为充足的依赖关系才能让Make等工具自动识别哪些文件需要重编译,哪些则不需要。一个常见的增量式编译问题就是,文件修改了,一部分文件重启编译,一部分没有,导致编译完成,但功能非常异常。
如果将缩进的TAB符换成了空格,make工具会提示missing separator
错误。
在上述的Makefile编写里面,把所有文件都写出来了,这个对于大量文件来说,很不现实。可以使用Makefile变量语法。
首先可以定义变量:
A = once
b = $(A)
c := $(A)
d ?= D
A = day
show:
echo $(b) $(c) $(d)
有三种赋值方式,结果如下:
=
赋值,类似引用,所以b的值为day,即A的最后有效值。:=
赋值,类似于值赋值,所以c的值为once,仅使用赋值之前的A值。?=
赋值,尝试性赋值,如果d的值不为空(完全空,空字符串不算),那么赋值为D.因此将所有的目标文件赋值到变量上:
objects = main.o tool.o debug.o
main : $(objects)
gcc -o main $(objects)
%.o : %.c
gcc -c $<
下面的%.o
等是模式匹配,即匹配所有后缀为.o
的文件,$<
是和模式匹配相对应的自动化变量,其代表了一类由模式匹配定义的文件集合。
在这里$<
表示符合%.c
定义的所有文件集合,即main.c tool.c debug.c
,这样就无须手动输入文件名字。
.PHONY : clean
clean:
rm *.o
如上所示,.PHONY
后面表示的是伪目标。对于伪目标,不用和真实的文件产生关联,当每次输入伪目标时,总是会执行该目标下命令。
随着开发环境的改变,需要在常见的X86平台上编译出能在ARM上运行的程序,这就是常见的交叉编译环境。可以直接使用ARM官方提供的工具链,也可以使用打包好的开发工具(apt-get)等下载。
编译器选择合适设备的即可,一般如下命名:
arm-none-linux-gnueabihf
arm-none-eabi
aarch64-none-elf
arm
是指定的架构,即ARM系列CPU,aarch64
是64位架构。none
这里是厂家指定的名字,这是ARM官方的,因此为none
。ebai
是嵌入式API接口的意义。hf
是带有硬件浮点数,代码会使用浮点指令。linux-gnu
指定平台,这个并非裸机开发接口,默认在Linux-gnu平台跑。在ubuntu下(需要换源)可以直接安装编译环境,但是版本会是最新的。
使用apt search arm-linux
和apt search aarch64-linux
分别查看32位和64位编译器(c,c++,go等)。
ubuntu系统可能需要换源,需要去国内源网站寻找合适的列表:
根据需要可下载指定的编译器:
sudo apt install gcc-aarch64-linux-gnu #64位
sudo apt install gcc-arm-linux-gnueabihf #32位
如下所示:
ubuntu->c-code:$ aarch64-linux-gnu-gcc symbol.c -o test64.out
ubuntu->c-code:$ file test64.out
test64.out: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=14313c53139a32bb1ad71e60439816744e04faab, for GNU/Linux 3.7.0, not stripped
在远程交叉服务器上可以使用SCP来传输文件,远程服务器要开启stfp-server功能,将本地设备的rsa公钥放在服务器上即可。如下:
ssh-keygen -t rsa -C "local-z3200"
,生成私钥和公钥,放在本地机器的合适位置。/home/username/.ssh/authorized_keys
文件中。scp
命令传输。SCP命令可参考:Linux scp命令 | 菜鸟教程 (runoob.com).
usage: scp [-346ABCOpqRrsTv] [-c cipher] [-D sftp_server_path] [-F ssh_config]
[-i identity_file] [-J destination] [-l limit]
[-o ssh_option] [-P port] [-S program] source ... target
简单参数如下:
-1
: 强制scp命令使用协议ssh1-2
: 强制scp命令使用协议ssh2-4
: 强制scp命令只使用IPv4寻址-6
: 强制scp命令只使用IPv6寻址-B
: 使用批处理模式(传输过程中不询问传输口令或短语)-C
: 允许压缩。(将-C标志传递给ssh,从而打开压缩功能)-p
:保留原文件的修改时间,访问时间和访问权限。-q
: 不显示传输进度条。-r
: 递归复制整个目录。-v
:详细方式显示输出。scp和ssh(1)会显示出整个过程的调试信息。这些信息用于调试连接,验证和配置问题。-c cipher
: 以cipher将数据传输进行加密,这个选项将直接传递给ssh。-F ssh_config
: 指定一个替代的ssh配置文件,此参数直接传递给ssh。-i identity_file
: 从指定文件中读取传输时使用的密钥文件,此参数直接传递给ssh。-l limit
: 限定用户所能使用的带宽,以Kbit/s为单位。-o ssh_option
: 如果习惯于使用ssh_config(5)中的参数传递方式,-P port
:注意是大写的P, port是指定数据传输用到的端口号-S program
: 指定加密传输时所使用的程序。此程序必须能够理解ssh(1)的选项。一般使用下面形式即可:
scp -i /var/flow-info/id_rsa source_file destination_file
如果目标文件位置在本机,那就是从服务器下载,如果目标位置在服务器,那就是上传文件。
scp -i /var/flow-info/id_rsa [email protected]:/home/ubuntu/c-code/test64.out test.out
上面即从远程服务器下载文件到本地机器上,root
是用户名,密钥需要和用户名对应上。
ubuntu->c-code:$ aarch64-linux-gnu-gcc-11 symbol.c -o test64.out
使用对应的执行程序即可。交叉编译器一般都有前缀来修饰,如下:
ubuntu->aarch64-linux-gnu:$ ll
total 20
drwxr-xr-x 5 root root 4096 Mar 12 15:52 ./
drwxr-xr-x 16 root root 4096 Mar 12 15:52 ../
drwxr-xr-x 2 root root 4096 Mar 12 15:52 bin/
drwxr-xr-x 32 root root 4096 Mar 12 15:52 include/
drwxr-xr-x 2 root root 4096 Mar 12 16:59 lib/
编译时,各类可执行文件(gcc, ar, ld等等),头文件(include),以及lib都放在了指定目录下。
不同的交叉编译器会生成不同的版本,因此需要根据实际需求来翻找目录。
将上列编译的二进制程序放在本地设备上运行,很大概率会出现以下错误:
onceday->shell:# ./test.out
./test.out: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by ./test.out)
这是因为编译服务器上的glibc版本较高导致的。解决该问题有多种方式,可参考:
解决方法很多,最常见的有以下几种:
接下来介绍两种方式,即修改动态库加载地址(默认地址是/usr/lib,这个是系统默认的库,不能改动,需要放在其他地方),以及编译指定版本的glibc库。
有多种方式,如patchelf
工具可以直接修改,首先安装该工具:
apt-get install patchelf
常见命令如下:
ubuntu->aarch64-linux-gnu:$ patchelf
syntax: patchelf
[--set-interpreter FILENAME]
[--print-interpreter]
[--set-rpath RPATH]
[--add-rpath RPATH]
[--remove-rpath]
[--print-rpath]
FILENAME...
这里主要使用下面两个命令:
patchelf --set-rpath /my/lib your_program
patchelf --set-interpreter /my/lib/ld-linux.so.2 your_program
路径可以使用绝对路径和相对路径两种。
如下所示,现在程序可以正常运行了(使用./lib库中的动态链接文件)。
onceday->shell:# ll lib/
total 1792
drwxr-xr-x 2 root root 4096 Mar 12 18:07 ./
drwxr-xr-x 3 root root 4096 Mar 12 18:00 ../
-rwxr-xr-x 1 root root 182488 Mar 12 18:07 ld-linux-aarch64.so.1*
-rw-r--r-- 1 root root 317 Mar 12 18:00 libc.so
-rw-r--r-- 1 root root 1635112 Mar 12 18:00 libc.so.6
onceday->shell:# file test.out
test.out: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter ./lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=14313c53139a32bb1ad71e60439816744e04faab, not stripped
除了使用patchelf工具改变ELF头信息之外,还可以编译时加入参数来更改。
# 绝对路径
gcc -Wl,-rpath='/my/lib',-dynamic-linker='/my/lib/ld-linux.so.2'
-Wl
,传递后面的选项到链接器中,ELF头信息的最终修改是在链接阶段落幕的。-rpath=dir
,指定动态库链接的文件目录,即指定的文件目录。-dynamic-linker=dir
,指定动态链接器的名字。这两个参数分别设置的elf文件中的rpath和interpreter字段。
rpath
,全名run-time search path
,是elf文件中一个字段,它指定了可执行文件执行时搜索so文件的第一优先位置,一般编译器默认将该字段设为空。elf文件中还有一个类似的字段runpath,其作用与rpath类似,但搜索优先级稍低。搜索优先级:
rpath > LD_LIBRARY_PATH > runpath > ldconfig缓存 > 默认的/lib,/usr/lib等
可以指定相对路径,如下,ld会将ORIGIN理解成可执行文件所在的路径:
gcc -Wl,-rpath='$ORIGIN/../lib'
下面是一个实例(-Wl
中W大写):
ubuntu->c-code:$ aarch64-linux-gnu-gcc-11 -Wl,-rpath='./lib',-dynamic-linker='./lib/ld-linux-aarch64.so.1' symbol.c -o test64.out
ubuntu->c-code:$ patchelf --print-interpreter test64.out
./lib/ld-linux-aarch64.so.1
可以看到,链接的动态库位置和链接器已经是预定目录了,然后打包程序的时候,按照需要打包动态库文件即可。
首先使用ldd
查看本地设备(目标设备)的glibc版本,如下:
onceday->shell:# ldd --version
ldd (GNU libc) 2.27
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
可以看到版本为2.27
,然后去官网下载对应版本的源码压缩包即可。
在本地设备上通过代理下载好了以后,可以使用scp传输到远程设备上。
PS D:\mysoft> scp .\glibc-2.27.tar.gz [email protected]:/home/ubuntu/
解压命令为tar -zxv -f glibc-2.27.tar.gz
,其他压缩格式需要修改对应的z
字符为j
或J
。
glibc库不能在所在目录中编译输出,需要额外创建目录,这里选在/usr/glibc2.27
:
sudo mkdir /usr/glibc2.27
sudo chmod 777 /usr/glibc2.27
需要安装一些依赖库,如下:
apt install make
apt install gawk
apt install texinfo
apt install gettext
apt install gcc-12-aarch64-linux-gnu
apt install g++-12-aarch64-linux-gnu
apt install bison
可查看INSTALL
文件,里面详细描述了如何编译和安装glibc库,必须具备完整的编译环境,一般而言,前面的交叉环境会搞定这个事情,然后如下即可:
../glibc-2.27/configure \
--prefix=/usr/glibc2.27/output \
CC=aarch64-linux-gnu-gcc-12 \
CXX=aarch64-linux-gnu-g++-12\
NM=aarch64-linux-gnu-gcc-nm-12\
READELF=aarch64-linux-gnu-readelf\
--host=aarch64-ntos-linux-gnu \
--build=x86_64-none-linux-gnu \
--with-headers=/usr/aarch64-linux-gnu/include \
--enable-kernel=4.14.0 \
--with-binutils=/usr/aarch64-linux-gnu/bin \
--disable-werror
--prefix=
,指定目标安装文件夹,需要注意,默认将安装到/usr/local
,这会导致不好的后果,切记指定一个其他目录。CC=\CXX=
,指定编译器,这里需要指定交叉编译器,即aarch64-linux-gnu-gcc-11
。--build=
,指定编译环境的本地系统,即用于编译的机器。--host=
,指定目标运行系统,即本地设备类型,命名规则可在glibc源码的readme文件中查看。--with-headers
,交叉编译需要使用目标系统上的Linux头文件。--enable-kernel=4.14.0
,指定最小运行版本号,这是针对Linux系统设置的。--with-binutils=
,使用指定的二进制工具包,即交叉编译所携带的工具。 --disable-werror
,跳过小错误,由于glibc版本和编译器版本不对应,有一些正常报错,可以忽略。开启编译,使用make -j 4
,表示使用多核编译,加快速度。
编译成功后,需要make install
生成目标安装文件,然后打包output
里面的文件,就是一个完整的glibc库了。
和使用默认开发路径的glibc库不同,本地编译的glibc库需要分开多部编译。
第一步是首先使用头文件和源文件生成目标文件:
aarch64-linux-gnu-gcc-11 -I~/include sybmol.c -o test2.o
这里~/include
即使本地编译的glibc库include目录。
然后链接动态加载器和标准C库,即如下:
aarch64-linux-gnu-gcc-11 test2.o ../lib/libc.so.6 ../lib/ld-linux-aarch64.so.1 -o test3.out
这样生成的可执行程序便是我们指定glibc版本的可执行程序。
(整个开发过程问题很多,难以详细写出,有问题可以留言,一起讨论)