嵌入式开发板IMX6UL上SSH移植实战:BusyBox-1.36.1环境下的完整配置流程

在嵌入式物联网项目的实际开发中,远程管理和调试是一个绕不开的刚需。想象一下,你的设备部署在工厂车间、户外机柜,甚至更偏远的地方,每次修改配置或排查问题都要抱着显示器、键盘去现场插拔,这效率恐怕会让项目进度举步维艰。这时候,一个稳定可靠的远程登录通道就成了开发者的“生命线”。SSH(Secure Shell)协议,凭借其加密传输和强大的功能,无疑是这条生命线的最佳选择。

然而,将SSH移植到资源受限的嵌入式环境,尤其是基于BusyBox构建的精简根文件系统时,情况就变得复杂起来。这不像在桌面Linux上apt install openssh-server那么简单。你需要面对交叉编译的依赖迷宫、库文件路径的精准配置,以及如何在极简的系统环境中让服务稳定自启。本文将以NXP i.MX6UL这款在工业控制、物联网网关领域广泛应用的处理器为例,结合BusyBox-1.36.1构建的根文件系统,手把手带你走通从零开始移植OpenSSH的全过程。我会分享在编译配置中踩过的坑、一些提升效率的脚本技巧,以及如何确保服务在目标板上安全、可靠地运行。无论你是正在为产品添加远程维护功能的嵌入式工程师,还是希望深入理解嵌入式Linux系统构建的学习者,这篇实战指南都将提供清晰的路径和可落地的细节。

1. 构建前的环境审视与准备工作

在动手编译之前,花些时间厘清环境,能避免后续很多“莫名其妙”的错误。嵌入式开发的核心特征之一就是交叉编译:我们在性能强大的x86主机(通常是Ubuntu)上,生成能在ARM架构目标板上运行的二进制文件。这个过程中,编译器、库路径、系统环境变量的任何错位都可能导致失败。

首先,确认你的交叉编译工具链已正确安装并配置。对于i.MX6UL,常见的工具链前缀是arm-linux-gnueabihf-(带硬件浮点支持)。你可以在终端输入以下命令验证:

arm-linux-gnueabihf-gcc --version

如果显示“command not found”,你需要先安装或配置工具链。很多芯片厂商(如NXP)会提供优化的工具链,也可以使用Linaro或Buildroot生成的工具链。确保其路径已加入系统的PATH环境变量。

其次,理解BusyBox环境的**“精简”意味着什么**。BusyBox将众多常用Unix工具集成进一个可执行文件,通过符号链接提供不同功能。它可能不包含标准的动态链接器缓存(ldconfig),也可能使用非常规的库文件路径(如/lib或/usr/lib可能是指向同一目录的链接)。在移植SSH这种依赖zlib和openssl等外部库的软件时,我们必须显式地告诉编译系统这些库的存放位置,并确保最终生成的二进制文件能在目标板的文件系统布局中找到它们。

提示:强烈建议在普通用户(非root)下进行所有编译操作。这能防止误操作污染系统目录,也符合安全开发规范。我们可以通过--prefix参数将所有编译产物安装到独立的用户目录下。

为此,我们建立一个清晰的工作目录结构。我习惯在用户主目录下创建一个项目专属文件夹:

mkdir -p ~/projects/imx6ul_ssh
cd ~/projects/imx6ul_ssh
mkdir -p source build install
  • source: 用于存放下载的源码包(zlib, openssl, openssh)。
  • build: 作为各源码包的编译目录(保持源码目录纯净)。
  • install: 作为交叉编译产物的安装目录,最终从这里提取文件打包到目标板。

接下来,我们需要获取三个核心组件的源代码。务必注意版本兼容性,较新版本的OpenSSH可能依赖特定版本的OpenSSL。这里我们选择一个经过验证的稳定组合:

组件推荐版本官方下载源(示例)备注
zlib1.2.13https://www.zlib.net/数据压缩库,SSH传输的基础
OpenSSL1.1.1whttps://www.openssl.org/source/加密库,提供SSH的加密算法支持
OpenSSH9.6p1https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/SSH协议的实现

将下载的tar.gz压缩包放入~/projects/imx6ul_ssh/source目录备用。准备工作就绪,我们可以开始逐个攻破依赖库的交叉编译。

2. 交叉编译依赖库:zlib与OpenSSL

SSH服务依赖于两个关键库:zlib用于数据压缩,提升传输效率;OpenSSL提供底层的加密算法(如AES, RSA)和TLS协议支持。我们必须先为ARM目标板正确编译它们。

2.1 编译zlib

zlib的编译配置相对简单,但有一个关键点:静态库与位置无关代码(PIC)。由于我们最终可能将库文件集成到BusyBox根文件系统,并可能被多个进程使用,编译为静态库(.a)并添加-fPIC选项是更稳妥的做法。

# 进入工作目录并解压源码
cd ~/projects/imx6ul_ssh
tar -xzf source/zlib-1.2.13.tar.gz -C build/
cd build/zlib-1.2.13

# 配置编译环境,指定安装前缀和交叉编译器
export CC=arm-linux-gnueabihf-gcc
export CFLAGS="-O2 -fPIC"
./configure --prefix=$(pwd)/../../install/zlib

# 直接修改Makefile中的关键变量,这是最可靠的方式
sed -i "s|^CC=.*|CC=arm-linux-gnueabihf-gcc -fPIC|" Makefile
sed -i "s|^LDSHARED=.*|LDSHARED=arm-linux-gnueabihf-gcc -shared -Wl,-soname,libz.so.1|" Makefile
sed -i "s|^CPP=.*|CPP=arm-linux-gnueabihf-gcc -E|" Makefile
sed -i "s|^AR=.*|AR=arm-linux-gnueabihf-ar|" Makefile

# 编译并安装到指定的install目录
make
make install

编译完成后,检查~/projects/imx6ul_ssh/install/zlib目录,应该能看到include, lib等子目录,lib目录下应有libz.a(静态库)和libz.so.x.x.x(动态库)。记录下这个路径,下一步编译OpenSSL时需要。

2.2 编译OpenSSL

OpenSSL的配置更为复杂,需要明确指定目标平台、禁用不适用于嵌入式环境的组件(如汇编优化),并正确链接我们刚编译的zlib。

cd ~/projects/imx6ul_ssh
tar -xzf source/openssl-1.1.1w.tar.gz -C build/
cd build/openssl-1.1.1w

# 使用Configure脚本进行配置(注意是大写C)
# linux-generic32: 指定为32位Linux通用平台
# no-asm: 禁用汇编代码,提高跨平台兼容性
# shared: 同时生成动态库
# --prefix: 指定安装路径
# --with-zlib-include/--with-zlib-lib: 指定zlib的头文件和库路径
./Configure linux-generic32 no-asm shared \
  --prefix=$(pwd)/../../install/openssl \
  --with-zlib-include=$(pwd)/../../install/zlib/include \
  --with-zlib-lib=$(pwd)/../../install/zlib/lib \
  --cross-compile-prefix=arm-linux-gnueabihf-

# 编译并安装
make
make install

注意:如果编译过程中出现关于zlib的错误,请回头确认zlib的路径是否正确,以及libz.so是否已生成。有时需要手动将install/zlib/lib加入LD_LIBRARY_PATH环境变量供编译过程链接,例如:export LD_LIBRARY_PATH=~/projects/imx6ul_ssh/install/zlib/lib:$LD_LIBRARY_PATH。

编译安装成功后,install/openssl目录下会包含完整的OpenSSL开发文件(bin, include, lib等)。至此,两大依赖库已准备完毕。

3. 交叉编译OpenSSH及其定制化配置

有了坚实的库文件基础,现在可以编译主角OpenSSH了。OpenSSH的配置脚本功能强大,我们需要通过参数精确控制其行为,使其适应嵌入式环境。

cd ~/projects/imx6ul_ssh
tar -xzf source/openssh-9.6p1.tar.gz -C build/
cd build/openssh-9.6p1

# 运行configure脚本,传递关键参数
./configure \
  --host=arm-linux-gnueabihf \
  --with-libs \
  --with-zlib=$(pwd)/../../install/zlib \
  --with-ssl-dir=$(pwd)/../../install/openssl \
  --disable-strip \
  --disable-etc-default-login \
  --disable-lastlog \
  --disable-utmp \
  --disable-utmpx \
  --disable-wtmp \
  --disable-wtmpx \
  --sysconfdir=/usr/local/etc \
  --prefix=/usr/local \
  CC=arm-linux-gnueabihf-gcc \
  AR=arm-linux-gnueabihf-ar

# 参数解析:
# --host: 指定目标平台
# --with-zlib/--with-ssl-dir: 指定依赖库路径
# --disable-*: 禁用一些在嵌入式环境中可能不必要的功能(如日志记录),以减小体积和依赖
# --sysconfdir/--prefix: 指定配置文件和安装路径前缀(基于目标板的路径视角)

配置完成后,运行make进行编译。这里只需要编译,不要执行make install,因为make install会尝试将文件安装到主机系统的/usr/local目录,这显然不是我们想要的。

编译成功后,当前目录下会生成我们需要的所有二进制文件(ssh, sshd, scp, sftp等)和配置文件模版。接下来是一个关键步骤:手动打包。我们需要创建一个脚本,智能地收集所有必需的文件,并按照目标板上的目录结构进行组织。

我编写了一个名为pack_ssh.sh的脚本,它不仅能复制文件,还会在必要时生成SSH服务所需的主机密钥。

#!/bin/bash
# pack_ssh.sh - 打包OpenSSH编译产物
set -e

BUILD_DIR="./" # 假设在openssh编译目录下运行
INSTALL_DIR="./output/usr"
CONFIG_DIR="${INSTALL_DIR}/local/etc"
BIN_DIR="${INSTALL_DIR}/local/bin"
SBIN_DIR="${INSTALL_DIR}/sbin"
LIBEXEC_DIR="${INSTALL_DIR}/libexec"

# 创建目录结构
mkdir -p ${CONFIG_DIR} ${BIN_DIR} ${SBIN_DIR} ${LIBEXEC_DIR}

# 复制客户端和服务端工具
CLIENT_TOOLS="scp sftp ssh ssh-add ssh-agent ssh-keygen ssh-keyscan"
for tool in ${CLIENT_TOOLS}; do
    if [ -f "${BUILD_DIR}/${tool}" ]; then
        cp "${BUILD_DIR}/${tool}" "${BIN_DIR}/"
        echo "[OK] Copied ${tool} to bin/"
    else
        echo "[ERROR] ${tool} not found!" >&2
        exit 1
    fi
done

# 复制服务器守护进程
if [ -f "${BUILD_DIR}/sshd" ]; then
    cp "${BUILD_DIR}/sshd" "${SBIN_DIR}/"
    echo "[OK] Copied sshd to sbin/"
else
    echo "[ERROR] sshd not found!" >&2
    exit 1
fi

# 复制辅助程序
HELPER_TOOLS="sftp-server ssh-keysign"
for helper in ${HELPER_TOOLS}; do
    if [ -f "${BUILD_DIR}/${helper}" ]; then
        cp "${BUILD_DIR}/${helper}" "${LIBEXEC_DIR}/"
        echo "[OK] Copied ${helper} to libexec/"
    else
        echo "[ERROR] ${helper} not found!" >&2
        exit 1
    fi
done

# 复制默认配置文件
CONFIG_FILES="moduli ssh_config sshd_config"
for config in ${CONFIG_FILES}; do
    if [ -f "${BUILD_DIR}/${config}" ]; then
        cp "${BUILD_DIR}/${config}" "${CONFIG_DIR}/"
        echo "[OK] Copied ${config} to etc/"
    else
        echo "[WARN] ${config} not found, using defaults later." >&2
    fi
done

# 生成SSH主机密钥(如果不存在)
KEY_TYPES="rsa dsa ecdsa ed25519"
for type in ${KEY_TYPES}; do
    KEY_FILE="ssh_host_${type}_key"
    if [ ! -f "${CONFIG_DIR}/${KEY_FILE}" ]; then
        echo "Generating ${KEY_FILE}..."
        # 使用编译出的ssh-keygen生成密钥
        ./ssh-keygen -t ${type} -f "${CONFIG_DIR}/${KEY_FILE}" -N "" -q
        chmod 600 "${CONFIG_DIR}/${KEY_FILE}"
        echo "[OK] Generated and copied ${KEY_FILE}"
    fi
done

echo "OpenSSH packaging completed in ${INSTALL_DIR%/usr}"

给脚本添加执行权限并运行:chmod +x pack_ssh.sh && ./pack_ssh.sh。脚本会在当前目录下创建一个output目录,里面就是整理好的usr目录树。同样地,我们需要将之前编译的zlib和openssl的动态库(.so文件)也收集起来。通常只需要复制libz.so.*和libssl.so.*、libcrypto.so.*到output/usr/lib目录。

最后,将整个output目录(或其中的usr目录)打包,准备传输到开发板。

cd ~/projects/imx6ul_ssh/build/openssh-9.6p1
tar -cjvf ../openssh_for_imx6ul.tar.bz2 -C output .

4. 在i.MX6UL开发板上的部署与调优

将打包好的openssh_for_imx6ul.tar.bz2、zlib和openssl的库文件包通过TFTP、NFS、U盘或SD卡拷贝到i.MX6UL开发板的根文件系统。假设都放在了/tmp目录。

第一步:解压并部署文件。

# 在开发板终端上操作
cd /tmp
tar -xjvf openssh_for_imx6ul.tar.bz2 -C /
# 解压后,/usr/local/bin, /usr/sbin, /usr/local/etc 等目录下应该就有了相应文件

# 确保动态库能被找到。如果BusyBox的ldconfig可用:
ldconfig
# 如果不可用,可以手动创建链接或直接将.so文件拷贝到/lib目录
cp /usr/lib/libz.so.* /lib/ 2>/dev/null || true
cp /usr/lib/libssl.so.* /lib/ 2>/dev/null || true
cp /usr/lib/libcrypto.so.* /lib/ 2>/dev/null || true

第二步:创建必要的系统账户和目录。

SSH服务为了安全,会使用一个特权分离的专用账户sshd。

# 在开发板的 /etc/passwd 文件末尾添加一行
echo "sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin" >> /etc/passwd

# 创建该用户的家目录(一个空目录)
mkdir -p /var/empty/sshd
chown root:root /var/empty/sshd
chmod 755 /var/empty/sshd

第三步:修改SSH服务器配置以允许root登录(仅限调试阶段,生产环境应禁用)。

编辑/usr/local/etc/sshd_config文件,找到关于PermitRootLogin的行并修改:

vi /usr/local/etc/sshd_config
# 将 #PermitRootLogin prohibit-password 改为
PermitRootLogin yes

注意:在生产环境中,强烈建议使用普通用户登录,然后通过su或sudo提权,而不是直接允许root SSH登录。这里为了方便测试才启用。

第四步:设置自启动脚本。

为了让SSH服务在开发板启动时自动运行,我们需要在BusyBox的初始化脚本中添加命令。通常,BusyBox会执行/etc/init.d/rcS脚本。

vi /etc/init.d/rcS
# 在文件末尾(或在网络启动之后)添加
/usr/sbin/sshd &

第五步:生成主机密钥(如果打包脚本未在目标板执行)。

如果之前打包时没有预生成密钥,或者密钥丢失,需要在目标板上生成:

/usr/local/bin/ssh-keygen -t rsa -f /usr/local/etc/ssh_host_rsa_key -N ""
/usr/local/bin/ssh-keygen -t ecdsa -f /usr/local/etc/ssh_host_ecdsa_key -N ""
/usr/local/bin/ssh-keygen -t ed25519 -f /usr/local/etc/ssh_host_ed25519_key -N ""

第六步:启动服务并测试。

# 手动启动sshd守护进程
/usr/sbin/sshd -f /usr/local/etc/sshd_config

# 检查进程是否运行
ps | grep sshd

# 为root用户设置一个密码(如果之前没有)
passwd root

现在,从你的开发电脑上,使用SSH客户端(如Linux/macOS的终端,Windows的PuTTY或MobaXterm)尝试连接开发板的IP地址。如果一切顺利,你应该能成功登录。首次连接时会提示接受主机密钥。

性能与安全调优考虑:

  1. 精简配置:再次审视sshd_config,关闭用不到的功能(如X11转发、TCP转发、GSSAPI认证),能减少攻击面。
  2. 密钥认证:相比于密码,使用公钥认证更安全。将开发电脑的公钥(~/.ssh/id_rsa.pub)内容添加到开发板的/root/.ssh/authorized_keys文件中。
  3. 日志监控:确保/var/log目录存在且可写,SSH的日志会输出到系统日志中,可通过logread(如果BusyBox支持)查看。
  4. 防火墙:如果开发板有防火墙(如iptables),确保开放22端口。

移植完成后,一个稳定、加密的远程管理通道就建立起来了。你可以用它来传输文件(scp/sftp)、远程执行命令,甚至进行端口转发,极大提升了开发和维护效率。整个过程虽然步骤繁多,但每一步都有其明确的目的,理解其背后的原理,就能在遇到其他类似软件的移植时举一反三。

更多推荐