SD-WAN终端研发系列03 OpenWrt编译体系:从源码到固件的全链路
本文是系列第3篇。前两篇分别从业务和系统原理的角度建立整体认知。本文进入实际工程领域——如何从OpenWrt源码出发,构建出包含自定义功能的固件。
一、为什么要从源码编译
OpenWrt官方提供了预编译固件,可以直接下载刷入设备。但在SD-WAN终端开发场景中,通常需要从源码编译,原因是:
- 内核定制:需要修改内核驱动(如添加4G模块支持)、内核参数(如启用调试符号)
- 默认配置:需要设置自定义的管理密码、默认IP地址、WiFi参数等
- 软件包集成:需要将自定义的软件包(如Agent程序及运行时)直接编译进固件
- 硬件适配:需要为新型号路由器添加设备支持
二、编译环境搭建
2.1 宿主机要求
OpenWrt编译需要在Linux环境下进行(推荐Ubuntu),且不要使用root用户。典型配置要求:
- CPU:高主频多核(影响编译速度)
- 内存:8GB以上
- 硬盘:40GB以上可用空间
- 网络:需要访问GitHub等外网资源(国内环境需配置代理或使用镜像源)
2.2 基础步骤
从零开始编译固件的完整流程如下:
# 1. 安装编译依赖
sudo apt-get install subversion g++ zlib1g-dev build-essential git \
python rsync man-db libncurses5-dev gawk gettext unzip \
file libssl-dev wget zip time curl
# 2. 获取源码
git clone https://github.com/openwrt/openwrt.git
cd openwrt
# 3. 选择版本分支
git tag # 查看可用版本标签
git checkout v19.07.8 # 切换到目标版本
# 4. 更新软件包源(feeds)
./scripts/feeds update -a # 下载所有feed源的软件包索引
./scripts/feeds install -a # 创建符号链接到package目录
# 5. 配置编译选项
make menuconfig # 弹出配置菜单
# 6. 下载源码包
make -j8 download V=s # 预下载所有依赖源码
# 7. 开始编译
make -j1 V=s # 首次编译建议单线程,方便定位错误
编译完成后,固件文件位于bin/targets/目录下。
三、编译目录结构
理解OpenWrt编译体系的关键是理解其目录结构。源码根目录下的主要目录及其作用:
openwrt/
├── config/ # 全局配置文件
├── dl/ # Download:从网络下载的源码压缩包
├── feeds/ # 外部软件包源的本地缓存
├── include/ # 编译系统使用的.mk文件
├── package/ # 软件包定义(Makefile和源码)
├── scripts/ # 编译辅助脚本
├── target/ # 平台相关代码(内核、设备树、分区表)
├── toolchain/ # 交叉编译工具链的构建定义
├── tools/ # 宿主机工具的构建定义
├── tmp/ # 编译临时文件
├── build_dir/ # 编译工作目录(解压、编译都在这里进行)
│ ├── host/ # 宿主机工具的编译
│ ├── toolchain-*/ # 交叉工具链的编译
│ └── target-*/ # 目标平台软件包的编译
├── staging_dir/ # 编译产物的安装目录
│ ├── host/ # 宿主机工具的安装位置
│ ├── toolchain-*/ # 交叉工具链的安装位置
│ └── target-*/ # 目标平台库文件的安装位置
└── bin/ # 最终输出目录
└── targets/ # 按平台分类的固件和ipk包
3.1 几个关键目录的关系
理解编译过程,需要关注以下三个目录的协作:
dl(download):编译过程中的源码下载缓存。从网络下载的源码压缩包会存放在这里。如果需要使用自己修改过的源码(比如打了4G驱动补丁的内核),可以制作一个同名压缩包放入此目录,编译时会优先使用本地文件。
build_dir:实际的编译工作区。源码从dl解压到这里,configure、make、make install都在此目录中进行。需要注意的是,执行make clean会清空此目录,所以直接在build_dir中修改文件是不可持续的——必须通过打补丁的方式来持久化修改。
staging_dir:编译产物的”仓库”。编译好的库文件、头文件会安装到此目录,供后续编译其他依赖它的软件包时使用。交叉工具链也安装在此目录下。
3.2 feeds机制
OpenWrt的软件包来自多个”feed”(软件源),每个feed是一个独立的Git仓库。默认配置包含以下feed:
- packages:基础库和工具(最常见的软件包来源)
- luci:Web管理界面(LuCI)
- routing:网络路由相关工具(如mwan3)
- telephony:语音通信相关工具
feeds源的定义文件是源码根目录下的feeds.conf.default。可以将其修改为自维护的仓库地址:
# 官方源
src-git packages https://git.openwrt.org/feed/packages.git;openwrt-19.07
# 自维护源(如内部仓库)
src-git packages https://git.example.com/packages.git;openwrt-19.07
也可以添加本地源,将自定义软件包直接放在本地目录中:
src-link myfeed /path/to/myfeed
更新和安装feed的操作:
./scripts/feeds update -a # 拉取所有feed的最新代码
./scripts/feeds install mypackage # 只安装指定包
./scripts/feeds install -a # 安装所有包
安装操作的本质是在package/feeds/目录下创建指向feeds/目录的符号链接。
四、单个软件包的编译过程
以编译一个名为lua的软件包为例,OpenWrt编译系统会将整个过程分为7个步骤:
步骤1:读取Makefile
读取 package/utils/lua/Makefile,获取包的定义信息
↓
步骤2:获取源码
如果是Git/SVN源,克隆源码到dl/目录,打包为 tar.gz
如果是URL源,直接下载到dl/目录
↓
步骤3:解压源码
将 dl/lua-x.x.x.tar.gz 解压到
build_dir/target-mips_xxx/lua-x.x.x/
↓
步骤4:配置(Configure)
在 build_dir 中执行 ./configure
(通过环境变量传入交叉编译参数)
↓
步骤5:编译(Compile)
在 build_dir 中执行 make
↓
步骤6:安装(Install)
将编译产物安装到
build_dir/target-mips_xxx/lua-x.x.x/ipkg-mipsel_24kc/
↓
步骤7:打包(Package)
将 ipkg 目录打包为 lua-x.x.x-1_mipsel_24kc.ipk
输出到 bin/mipsel_24kc/packages/base/
编译单个包的命令:
make package/lua/compile V=s # 编译lua包
make package/lua/clean # 清理lua包的编译产物
make package/lua/{clean,compile} # 清理后重新编译
五、menuconfig配置详解
make menuconfig弹出的配置界面是整个编译流程的控制中心。主要配置项包括:
5.1 目标平台选择
Target System → 处理器架构(如MediaTek Ralink MIPS)
Subtarget → 具体子系列(如MT76x8)
Target Profile → 设备型号(如具体的路由器型号)
这三项决定了编译出的固件适用于哪种硬件。选错目标平台,固件将无法在设备上运行。
5.2 软件包选择
每个软件包有三种状态:
<*>:编译并集成到固件中(默认选中,固件包含此包)<M>:编译但不集成到固件(单独生成ipk文件,可以后续手动安装)< >:不编译
在搜索包时可以使用/键搜索,然后按数字键跳转到对应位置。
5.3 固件镜像配置
Target Images
├── Kernel partition size (MB) # 内核分区大小,默认较小
├── Root filesystem partition size # 根文件系统分区大小
└── ... # 其他镜像选项
如果编译时选择了大量内核模块,默认的内核分区大小(2MB)可能不够,需要适当增大,否则系统启动时会找不到内核。
六、编写OpenWrt软件包的Makefile
当需要将自己开发的软件打包到OpenWrt中时,需要编写符合OpenWrt规范的Makefile。
6.1 基本模板
include $(TOPDIR)/rules.mk
# 包的基本信息
PKG_NAME:=mypackage
PKG_VERSION:=1.0
PKG_RELEASE:=1
PKG_BUILD_DIR:= $(BUILD_DIR)/$(PKG_NAME)
include $(INCLUDE_DIR)/package.mk
# 包的元数据(在menuconfig中显示)
define Package/$(PKG_NAME)
SECTION:=utils
CATEGORY:=Utilities
TITLE:=My custom package
DEPENDS:=+libc +libpthread # 依赖包(+号表示自动选中)
PKGARCH:=all # 目标架构(all表示不依赖具体平台)
MAINTAINER:=author@example.com
endef
define Package/$(PKG_NAME)/description
A detailed description of my custom package.
endef
# 编译前的准备工作(留空表示无需准备)
define Build/Prepare
endef
# 编译步骤(留空表示无需编译)
define Build/Compile
endef
# 安装到固件中(将文件复制到目标根目录)
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) ./files/myprogram $(1)/usr/bin/
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/myconfig $(1)/etc/config/
endef
# 编译入口
$(eval $(call BuildPackage,$(PKG_NAME)))
6.2 关键变量说明
$(1):代表目标系统的根目录(即固件中的/)$(INSTALL_DIR):创建目录(权限0755)$(INSTALL_BIN):复制可执行文件(权限0755)$(INSTALL_DATA):复制数据文件(权限0644)$(INSTALL_CONF):复制配置文件(权限0600)$(CP):复制文件或目录$(RM):删除文件或目录
6.3 安装前后脚本
Makefile支持定义包安装和卸载时自动执行的脚本:
# 安装前执行
define Package/$(PKG_NAME)/preinst
#!/bin/sh
echo "Before installing mypackage..."
endef
# 安装后执行(常用于创建软链接、设置权限)
define Package/$(PKG_NAME)/postinst
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
echo "Enabling mypackage service..."
/etc/init.d/mypackage enable
fi
endef
# 卸载前执行
define Package/$(PKG_NAME)/prerm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
/etc/init.d/mypackage disable
fi
endef
注意$${IPKG_INSTROOT}变量的用途:当系统在固件编译过程中安装包时(不是在真实设备上安装),这个变量有值,脚本会跳过服务操作。只有在真实设备上通过opkg安装时,才会执行服务启用/禁用。
6.4 实际示例:打包预编译的运行时环境
在SD-WAN终端中,Agent的运行时环境以预编译包的形式集成。以下是实际使用的Makefile简化版:
PKG_NAME:=sdwan-runtime-env
PKG_VERSION:=1
PKG_RELEASE:=2
define Package/$(PKG_NAME)
SECTION:=lang
CATEGORY:=Languages
SUBMENU:=Scripting
PKGARCH:=all # 预编译包不依赖特定平台
DEPENDS:=+libpthread +libbz2 +libopenssl +libffi +libsqlite3 +libuci +zlib
TITLE:=Runtime environment for SD-WAN agent
endef
# 无需编译,直接安装
define Build/Compile
endef
# 将预编译文件复制到目标位置
define Package/$(PKG_NAME)/install
$(CP) ./files/* $(1)/
# 创建软链接
$(LN) runtime $(1)/usr/bin/runtime
$(LN) libruntime.so.1.0 $(1)/usr/lib/libruntime.so
endef
七、SDK与Toolchain:应用层开发
如果不需要修改内核,只需要开发应用程序,可以使用SDK(Software Development Kit)而不是完整的源码工程。
7.1 SDK vs 源码工程
| 对比项 | 完整源码工程 | SDK |
|---|---|---|
| 内核编译 | 支持 | 不支持 |
| 应用开发 | 支持 | 支持 |
| 包含的软件包 | 全部 | 仅平台相关的基础包 |
| feeds | 包含 | 不包含 |
| 体积 | 大(数GB) | 较小 |
| 适用场景 | 固件定制、内核开发 | 应用层ipk包开发 |
SDK的获取方式有两种:
- 从OpenWrt官网下载对应平台和版本的预编译SDK
- 在源码工程的
make menuconfig中选择编译SDK,输出文件在bin/目录下
7.2 Toolchain(交叉编译工具链)
Toolchain是交叉编译的核心工具集。它包含了在x86主机上编译MIPS/ARM目标程序所需的所有工具——编译器(gcc)、链接器(ld)、汇编器(as)等。
使用Toolchain之前,需要配置环境变量:
# 将Toolchain的bin目录加入PATH
export PATH=/opt/OpenWrt-Toolchain-ramips-mt7628_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-i686/toolchain-mipsel_24kec+dsp_gcc-4.8-linaro_uClibc-0.9.33.2/bin:$PATH
# 设置staging目录(编译时查找头文件和库文件的路径)
export STAGING_DIR=/path/to/openwrt/staging_dir
# 使变量生效
source /etc/bash.bashrc
验证安装:
mipsel-openwrt-linux-gcc -v # 应输出交叉编译器版本信息
7.3 C运行库的选择
OpenWrt使用的C标准库会影响交叉编译的兼容性:
| C库 | 特点 | 适用场景 |
|---|---|---|
| glibc | 功能最全,体积最大 | 桌面/服务器系统 |
| uClibc | 专为嵌入式设计,体积小 | 不支持MMU的微处理器 |
| musl libc | 轻量级,标准兼容性好 | OpenWrt默认选择 |
OpenWrt从较新版本开始默认使用musl libc。它的设计目标是作为glibc的轻量替代品,同时保持良好的POSIX标准兼容性。在交叉编译时,需要确保编译器工具链和目标设备上的C库版本一致。
八、手动打包ipk
在某些场景下,可能需要跳过编译系统,直接将准备好的文件打包为ipk格式(比如将交叉编译好的运行时库打包):
# 1. 创建临时目录结构
mkdir -p /tmp/mypackage/CONTROL
mkdir -p /tmp/mypackage/usr/bin
mkdir -p /tmp/mypackage/usr/lib
# 2. 放入程序文件
cp myprogram /tmp/mypackage/usr/bin/
# 3. 编写CONTROL控制文件
cat > /tmp/mypackage/CONTROL/control << EOF
Package: mypackage
Version: 1.0.0-1
Depends: libc, libpthread
Architecture: mipsel_24kc
Installed-Size: 102400
Section: utils
Description: My custom package for OpenWrt
EOF
# 4. 打包
scripts/ipkg-build -o root -g root /tmp/mypackage /tmp/
注意事项:
Architecture字段需要与目标平台匹配,可通过opkg print-architecture查看- 在Ubuntu环境下打包时,
ipkg-build脚本中调用的date命令格式可能不兼容,需要在脚本头部添加alias date="date \"+%Y-%m-%d %H:%M:%S\""来修复
九、常见编译问题
9.1 编译清理命令
make clean # 清理bin和build_dir(保留配置和toolchain)
make dirclean # 额外清理staging_dir和toolchain
make distclean # 彻底清理(包括配置和下载的源码,慎用)
9.2 内核补丁的持久化
在build_dir中修改的内核文件,执行make clean后会丢失。正确的做法是:
- 保存修改前的原始文件
- 修改文件
- 使用
diff -ruN生成补丁文件 - 将补丁文件放入
target/linux/<board>/patches/目录
这样每次编译时,补丁会自动应用到内核源码上。
9.3 查找依赖关系
编译时如果提示缺少某个库文件,可以通过以下方式定位依赖来源:
cd staging_dir/target-*/pkginfo
grep "libxxx.so" *.provides # 查找哪个包提供了该库
下一篇将进入定制化开发领域——如何修改OpenWrt的默认配置、网络初始化流程和MAC地址设置,让固件一开机就符合SD-WAN终端的要求。