4、配置编译环境:环境变量、目录结构与sysroot
好,到了这一步,我们终于要开始动手搭建编译环境了。说实话,很多初学者在这一步容易翻车——不是环境变量写错了,就是目录结构搞乱了。我自己刚开始做嵌入式的时候,就因为在 PATH 里少写了一个斜杠,折腾了整整一个下午。
今天我们要搞定三件事:设置环境变量、创建构建目录、理解 sysroot。这三件事是连在一起的,说白了就是给工具链找个家,然后告诉它「你家在哪」。
4.1 环境变量:工具链的「导航系统」
先说说环境变量。你想想看,编译内核的时候,编译器怎么知道用哪个架构?怎么找到头文件和库?全靠环境变量指路。
我个人习惯把下面三个变量作为标配:
- PREFIX:工具链的安装路径。比如
/opt/toolchains/arm-cortex_a8-linux-gnueabi - TARGET:目标架构的 triplet。比如
arm-cortex_a8-linux-gnueabi - SYSROOT:目标系统的根文件系统路径。比如
$PREFIX/$TARGET/sysroot
为什么要这么分?我举个例子你就明白了。假设你要给一块 ARM 开发板编译内核,你的电脑是 x86 的。编译器本身跑在 x86 上,但它生成的代码是 ARM 的。那它去哪里找 ARM 的 stdio.h?去 /usr/include 肯定不行,那是 x86 的。所以你需要 SYSROOT 告诉它:「嘿,去这个目录找 ARM 的头文件」。
核心配置示例:
export PREFIX=/opt/toolchains
export TARGET=arm-cortex_a8-linux-gnueabi
export SYSROOT=$PREFIX/$TARGET/sysroot
export PATH=$PREFIX/bin:$PATH
export CROSS_COMPILE=$TARGET-
export ARCH=arm
这里有个坑,我曾经踩过:CROSS_COMPILE 变量末尾一定要带短横线 -。比如 arm-linux-gnueabi-,这样内核的 Makefile 才能自动拼出 arm-linux-gnueabi-gcc。少写了这个横线,编译会直接报错找不到编译器。
4.2 创建构建目录结构
环境变量设好了,接下来要搭目录架子。我习惯用这样的结构:
$PREFIX/
├── bin/ # 工具链可执行文件
├── $TARGET/
│ ├── bin/ # 目标架构的工具
│ ├── lib/ # 目标架构的库文件
│ ├── include/ # 目标架构的头文件
│ └── sysroot/ # 目标系统的根文件系统
│ ├── usr/
│ │ ├── lib/
│ │ ├── include/
│ │ └── bin/
│ ├── lib/
│ └── etc/
└── src/ # 源码包(可选)
├── binutils/
├── gcc/
└── linux/
为什么要这么分?嗯,这里有个设计思路:bin/ 放的是你电脑上直接运行的交叉编译工具,比如 arm-linux-gnueabi-gcc。而 $TARGET/ 下面的东西,是给目标板用的。说白了,一个是「厨师」,一个是「菜」。
创建这些目录很简单,一条命令搞定:
mkdir -p $PREFIX/{bin,$TARGET/{bin,lib,include,sysroot/{usr/{lib,include,bin},lib,etc}},src}
我个人建议把 src/ 也放进来。为什么?因为以后你要调试工具链或者重新编译某个组件,源码就在手边,不用到处找。我在项目里吃过这个亏——半年后要改一个编译选项,结果源码包不知道丢哪了,只能重新下载。
4.3 理解 sysroot 的概念
好,重点来了。sysroot 这个概念,说白了就是「目标系统的根目录的镜像」。你想想看,当你交叉编译一个程序时,这个程序最终要跑在目标板上。它依赖的 libc.so、ld-linux.so 这些动态库,还有各种头文件,都来自目标系统。
但你的编译环境是 x86 的,没有这些 ARM 的库。所以你需要把目标板的根文件系统「复制」一份到你的开发机上,放在 sysroot 目录下。编译器通过 --sysroot 参数就知道去哪里找这些文件。
避坑指南:我曾经遇到过一个问题——编译出来的程序在目标板上运行时报错 No such file or directory,但文件明明存在。后来发现是 sysroot 里的动态链接器路径不对。目标板的 /lib/ld-linux.so.3 在 sysroot 里变成了 /opt/toolchains/.../sysroot/lib/ld-linux.so.3。编译器在编译时会把路径写死,所以一定要确保 sysroot 里的路径结构和目标板完全一致。
sysroot 的典型内容长这样:
$SYSROOT/
├── usr/
│ ├── lib/
│ │ ├── libc.so.6
│ │ ├── libm.so.6
│ │ └── crt0.o
│ ├── include/
│ │ ├── stdio.h
│ │ ├── stdlib.h
│ │ └── ...
│ └── bin/
│ └── ...
├── lib/
│ ├── ld-linux.so.3
│ └── modules/
└── etc/
└── ...
你可能会问:这些文件从哪里来?两种方式:
- 从目标板拷贝:如果你已经有跑起来的开发板,直接
scp整个根文件系统过来。 - 用 buildroot 或 yocto 生成:这些工具会自动生成完整的 sysroot。
我个人更推荐第二种。因为从目标板拷贝容易漏文件,而且版本可能不一致。用 buildroot 生成的话,所有东西都是配套的,省心很多。
4.4 验证环境配置
配置完了,怎么知道对不对?我一般跑两个检查:
检查一:环境变量是否生效
echo $PREFIX
echo $TARGET
echo $SYSROOT
echo $CROSS_COMPILE
检查二:编译器能否找到 sysroot
${CROSS_COMPILE}gcc --print-sysroot
# 应该输出:/opt/toolchains/arm-cortex_a8-linux-gnueabi/sysroot
如果 --print-sysroot 输出的是空或者路径不对,说明你的编译器编译时没有配置 --with-sysroot。这时候你可以在编译内核时手动指定:
make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabi- \
KCFLAGS="--sysroot=$SYSROOT"
嗯,这里要注意:内核编译时不一定需要 sysroot,因为内核本身不链接用户空间的库。但如果你要编译内核模块,或者某些内核配置依赖头文件,sysroot 就派上用场了。
4.5 知识体系总览
为了让你更直观地理解这三者的关系,我画了一张图:
这张图把今天的内容串起来了。你看,PREFIX 定义了所有东西的根目录,TARGET 告诉编译器目标架构是什么,SYSROOT 提供了目标系统的头文件和库。三者缺一不可。
重要提醒:环境变量配置完成后,建议把它写入 ~/.bashrc 或 ~/.profile,这样每次打开终端就不用重新设置了。我习惯单独写一个脚本文件 setenv.sh,需要时 source 一下,避免污染全局环境。
好了,环境配置这块就讲到这里。记住:环境变量是导航,目录结构是骨架,sysroot 是灵魂。这三样搞定了,后面的编译工作就顺风顺水了。
公众号:蓝海资料掘金营,微信deep3321