Linux编译之C语言基础
Author:Once Day Date:2023年3月11日
漫漫长路,才刚刚开始…
1.概述
在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
错误。
1.2 使用makefile自动化变量
在上述的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
,这样就无须手动输入文件名字。
1.3 伪目标
.PHONY : clean
clean:
rm *.o
如上所示,.PHONY
后面表示的是伪目标。对于伪目标,不用和真实的文件产生关联,当每次输入伪目标时,总是会执行该目标下命令。
2.交叉编译环境
随着开发环境的改变,需要在X86平台上编译出能在ARM上运行的程序,这就是常见的交叉编译环境。可以直接使用ARM官方提供的工具链,也可以使用打包好的开发工具(apt-get)等下载。
- Arm GNU Toolchain Downloads – Arm Developer
- Ubuntu20.04配置交叉编译环境arm-linux-gcc_apt-get install arm-linux_蜻蜓队长~的博客-CSDN博客
编译器选择合适设备的即可,一般如下命名:
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系统可能需要换源,需要去国内源网站寻找合适的列表:
- LUG’s repo file generator (ustc.edu)
- ubuntu换镜像源(ubuntu换源)_Fighting_1997的博客-CSDN博客
根据需要可下载指定的编译器:
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
2.1 在本地机器和远程编译服务器上传入内容
在远程交叉服务器上可以使用SCP来传输文件,远程服务器要开启stfp-server功能,将本地设备的rsa公钥放在服务器上即可。如下:
ssh-keygen -t rsa -C "local-z3200"
,生成私钥和公钥,放在本地机器的合适位置。- 将公钥(id_rsa.pub)内容复制到远程服务器的
/home/username/.ssh/authorized_keys
文件中。 - 然后使用
scp
命令传输。
SCP命令可参考:Linux scp命令 | 菜鸟教程 (runoob).
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 root@onceday.work:/home/ubuntu/c-code/test64.out test.out
上面即从远程服务器下载文件到本地机器上,root
是用户名,密钥需要和用户名对应上。
2.2 使用交叉编译器进行编译
ubuntu->c-code:$ aarch64-linux-gnu-gcc-11 symbol.c -o test64.out
使用对应的执行程序即可。交叉编译器一般都有前缀来修饰,如下(文件目录,/usr/aarch64-linux-gnu):
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版本较高导致的。解决该问题有多种方式,可参考:
- glibc 下载及编译_glibc下载_kalaneryok的博客-CSDN博客
- glibc编译小记 | Marvin’s Blog【程式人生】 (marvinsblog)
- 【gcc】高版本gcc编译出的程序在低版本glibc机器上运行_高版本gcc编译 低版本运行_bandaoyu的博客-CSDN博客
- 低版本 libc 中运行高本版 libc 库链接的程序_patchelf chroot_longyu_wlz的博客-CSDN博客
- 多个gcc/glibc版本的共存及指定gcc版本的编译_gcc编译指定glibc版本_mo4776的博客-CSDN博客
解决方法很多,最常见的有以下几种:
- 升级本机设备的GLIBC版本,可行程度一般。
- 修改可执行程序的设置,改变动态库加载地方和GLIBC库引用地方,这个相当于整体打包静态库一起发布,程序使用自身的库,可行程度较高。
- 自行编译合适版本GLIBC库,然后用于编译,步骤较为麻烦,但效果应该最好。
- 各种骚操作修改编译过程和二进制指令,不太推荐,适合于特殊情况。
接下来介绍两种方式,即修改动态库加载地址(默认地址是/usr/lib,这个是系统默认的库,不能改动,需要放在其他地方),以及编译指定版本的glibc库。
2.3 修改可执行目标文件中的动态库解析器指向的位置
有多种方式,如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
可以看到,链接的动态库位置和链接器已经是预定目录了,然后打包程序的时候,按照需要打包动态库文件即可。
2.4 编译指定版本的glibc库文件
- glibc库的交叉编译及使用_yebanguhe的博客-CSDN博客
- glibc 下载及编译_glibc下载_kalaneryok的博客-CSDN博客
首先使用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
,然后去官网下载对应版本的源码压缩包即可。
http://ftp.gnu/pub/gnu/glibc/
官方下载,速度有点慢,可以使用代理加加速。https://downloads.uclibc-ng/releases/
,uclibc的下载点,另外一个libc库的选择。
在本地设备上通过代理下载好了以后,可以使用scp传输到远程设备上。
PS D:\mysoft> scp .\glibc-2.27.tar.gz ubuntu@onceday.work:/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库了。
2.5 使用本地编译的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
这里~/lib/
即使本地编译的glibc库lib目录。
这样生成的可执行程序便是我们指定glibc版本的可执行程序。
(整个开发过程问题很多,难以详细写出,有问题可以留言,一起讨论)
更多推荐
Linux编译之(1)C语言基础
发布评论