10、Sysroot概念:什么是sysroot、CMAKE_SYSROOT变量、如何指定系统根目录

好,咱们今天聊一个在交叉编译里绕不开的话题——sysroot

我记得刚入行那会儿,第一次做ARM Linux的交叉编译,折腾了好几天,编译出来的程序放到开发板上就是跑不起来。报错信息千奇百怪,什么"找不到libc.so.6"、"段错误"……后来一位老同事看了一眼我的CMakeLists.txt,叹了口气说:"你连sysroot都没设,怎么可能跑得起来?"

嗯,从那以后,我就把sysroot这事儿刻在脑子里了。

什么是sysroot?

说白了,sysroot就是目标系统的根文件系统的一个副本,放在你的宿主机上。

你想想看,你在PC上编译一个程序,这个程序最终是要跑到ARM开发板或者某个嵌入式设备上的。那编译的时候,链接器需要知道目标系统里有哪些库、头文件放在哪儿、版本对不对。你不能拿PC上的/lib和/usr/include去链接吧?那链接出来的东西肯定跑不通。

所以,我们需要一个"假的根目录"——sysroot。它里面装着目标系统需要的所有东西:

  • 动态库(.so文件)
  • 静态库(.a文件)
  • 头文件(.h文件)
  • 还有一些配置文件、运行时数据

我在项目中遇到过一种情况:同一个芯片方案,不同厂商提供的BSP里,libc的版本都不一样。如果你不指定正确的sysroot,链接器可能会从宿主机上拉一个高版本的glibc函数进来,结果目标系统上根本没有这个符号——程序一跑就崩。

核心理解:sysroot = 目标系统的"影子文件系统"。编译器和链接器看到这个影子,就知道目标系统长什么样。

CMAKE_SYSROOT变量

在CMake里,我们通过CMAKE_SYSROOT这个变量来告诉构建系统:"嘿,目标系统的根目录在这儿呢!"

它的用法很简单,一般在工具链文件(toolchain file)里设置:

# 设置sysroot路径
set(CMAKE_SYSROOT /path/to/your/sysroot)

# 或者更常见的,从环境变量读取
set(CMAKE_SYSROOT $ENV{SYSROOT_DIR})

设了这个变量之后,CMake会做几件事:

  • 告诉编译器:找头文件时,先去$CMAKE_SYSROOT/usr/include
  • 告诉链接器:找库文件时,先去$CMAKE_SYSROOT/lib$CMAKE_SYSROOT/usr/lib
  • 自动给编译器和链接器加上--sysroot参数

我个人习惯在工具链文件里这样写:

# 工具链文件: arm-linux-gnueabihf.cmake

# 先定义交叉编译器前缀
set(TOOLCHAIN_PREFIX arm-linux-gnueabihf)

# 定义sysroot路径
set(SYSROOT_PATH /opt/${TOOLCHAIN_PREFIX}/sysroot)

# 设置CMAKE_SYSROOT
set(CMAKE_SYSROOT ${SYSROOT_PATH})

# 指定编译器
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)

# 其他必要设置
set(CMAKE_FIND_ROOT_PATH ${SYSROOT_PATH})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

小提示:CMAKE_FIND_ROOT_PATHCMAKE_SYSROOT经常一起出现。前者告诉CMake在哪些路径下搜索库和头文件,后者告诉编译器使用哪个sysroot。两者配合使用效果最好。

如何指定系统根目录?

指定sysroot的方式,其实不止一种。我给大家梳理一下:

方式一:在工具链文件中设置(推荐)

就像上面代码里写的,直接在工具链文件里用set(CMAKE_SYSROOT ...)。这是最规范的做法,也是我一直在用的。

方式二:命令行传递

如果你不想写死在工具链文件里,也可以在调用cmake时传参:

cmake -DCMAKE_SYSROOT=/opt/sysroot -DCMAKE_TOOLCHAIN_FILE=my_toolchain.cmake ..

这种方式灵活,但容易忘。我曾经有个项目,团队成员各自传了不同的sysroot路径,结果链接出来的二进制文件五花八门……后来统一改到工具链文件里,世界清净了。

方式三:环境变量

有些项目习惯用环境变量:

export SYSROOT_DIR=/opt/my_sysroot
cmake -DCMAKE_SYSROOT=$SYSROOT_DIR ..

嗯,这种方式我不太推荐。环境变量太隐式了,换台机器或者换个终端窗口,环境变量没设对,编译就炸了。你想想看,排查起来多痛苦。

Sysroot的目录结构长什么样?

一个典型的sysroot目录结构大概是这样的:

/opt/arm-linux-gnueabihf/sysroot/
├── bin/
├── dev/
├── etc/
├── lib/
│   ├── ld-linux-armhf.so.3
│   ├── libc.so.6
│   ├── libm.so.6
│   └── ...
├── sbin/
├── usr/
│   ├── bin/
│   ├── include/
│   │   ├── stdio.h
│   │   ├── stdlib.h
│   │   └── ...
│   ├── lib/
│   │   ├── libpthread.so
│   │   ├── libstdc++.so
│   │   └── ...
│   └── share/
└── var/

看到没?它几乎就是目标系统根目录的一个"快照"。编译时,编译器会把/usr/include映射到$CMAKE_SYSROOT/usr/include,链接器会把/lib映射到$CMAKE_SYSROOT/lib

注意:sysroot里的库和头文件,必须和你目标系统上跑的系统完全一致。版本不对、架构不对,编译出来的程序要么链接失败,要么运行时崩溃。我曾经吃过这个亏——用了高版本的glibc编译,结果目标系统上的glibc版本低,程序一启动就报"FATAL: kernel too old"。

Sysroot的核心逻辑

为了让你更直观地理解,我画了一张图:

Sysroot 核心逻辑示意图 宿主机 (x86_64 PC) 交叉编译器 arm-linux-gnueabihf-gcc 链接器、汇编器... --sysroot Sysroot (系统根目录副本) /opt/arm-linux-gnueabihf/sysroot/ ├── lib/ (libc.so.6, ld-linux.so...) ├── usr/include/ (stdio.h, stdlib.h...) └── usr/lib/ (libpthread.so, libstdc++...) 部署到目标系统 目标系统 (ARM 开发板) 运行编译好的程序 依赖 libc.so.6, libpthread.so... 编译时,编译器通过 --sysroot 参数 从 Sysroot 中获取头文件和库 而不是从宿主机本地的 /usr 中获取

这张图想表达的意思很简单:编译时看sysroot,运行时看目标系统。两者必须保持一致,否则就会出问题。

如何获取或制作sysroot?

实际项目中,sysroot通常有几种来源:

来源方式 说明 适用场景
BSP/SDK 自带 芯片厂商提供的SDK里直接包含了sysroot 最省心,直接用就行
Yocto/Buildroot 构建 用Yocto或Buildroot编译整个系统时,会自动生成sysroot 定制化程度高的项目
从目标系统拷贝 在开发板上用rsync或tar打包根文件系统,拷回宿主机 快速验证、原型开发
交叉编译工具链自带 有些工具链(如Linaro)的发布包内已经包含了sysroot 标准Linux系统开发

我个人最常用的是Yocto生成的sysroot。Yocto会为每个recipe生成一个独立的sysroot,里面只包含这个recipe需要的依赖,干净又清爽。不过Yocto的学习曲线有点陡,刚开始用的时候我也被折腾得够呛。

一个实用技巧:如果你从目标系统拷贝sysroot,记得用rsync -a而不是cp。因为rsync能保留软链接、权限、时间戳,这些东西在sysroot里很重要。我曾经用cp -r拷过一次,结果所有软链接都变成了普通文件,链接器直接报错……

避坑指南

嗯,这里我要多说几句。关于sysroot,有几个坑我踩过,你们注意一下:

  • 不要混用不同架构的sysroot——ARM的sysroot不能给MIPS用,这是常识,但有时候项目多了容易搞混。我建议在sysroot目录名里带上架构信息,比如sysroot-armv7a-hfsysroot-aarch64
  • 注意sysroot里的libc版本——glibc是向后兼容的,但向前不兼容。用高版本glibc编译的程序,在低版本glibc的系统上跑不了。
  • CMAKE_SYSROOT和CMAKE_FIND_ROOT_PATH要配合使用——只设CMAKE_SYSROOT不设CMAKE_FIND_ROOT_PATH,CMake在find_package、find_library时可能还是会去宿主机路径下找。
  • 检查sysroot的完整性——有时候sysroot拷过来缺了几个库文件,编译时可能不报错(因为静态链接了),但运行时就会崩溃。我习惯在编译完成后,用readelf -d或者arm-linux-gnueabihf-readelf -d检查一下可执行文件的动态库依赖,确保所有依赖都在sysroot里存在。

好了,关于sysroot的概念、CMAKE_SYSROOT变量以及如何指定系统根目录,就讲到这里。这东西说白了就是一个"替身",让交叉编译器在宿主机上就能看到目标系统的样子。理解了这一点,后面很多交叉编译的问题就迎刃而解了。


公众号:蓝海资料掘金营,微信deep3321