Linux编译之(1)C语言基础

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-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-linuxapt 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
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.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是用户名,密钥需要和用户名对应上。

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库引用地方,这个相当于整体打包静态库一起发布,程序使用自身的库,可行程度较高。
  • 自行编译合适版本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库文件

首先使用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.org/pub/gnu/glibc/ 官方下载,速度有点慢,可以使用代理加加速。
  • https://downloads.uclibc-ng.org/releases/,uclibc的下载点,另外一个libc库的选择。

在本地设备上通过代理下载好了以后,可以使用scp传输到远程设备上

PS D:\mysoft> scp .\glibc-2.27.tar.gz [email protected]:/home/ubuntu/  

解压命令为tar -zxv -f glibc-2.27.tar.gz ,其他压缩格式需要修改对应的z字符为jJ

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版本的可执行程序

(整个开发过程问题很多,难以详细写出,有问题可以留言,一起讨论)

猜你喜欢

转载自blog.csdn.net/Once_day/article/details/129484023