使用 GCJ 的嵌入式 Java
本文讨论了如何在嵌入式 Linux 项目中使用 GCC 编译器套件的一部分 GCJ。像所有工具一样,GCJ 也有其优点,即能够使用像 Java 这样的高级语言进行编码,以及其缺点。最初,在嵌入式目标上运行 GCJ 的想法可能令人生畏,但您会发现这样做所需的工作量比您想象的要少。
阅读本文后,您应该受到启发,尝试在目标上进行试验,看看 GCJ 是否适合您的下一个项目。Java 语言具有各种巧妙的功能,例如自动垃圾回收、广泛而强大的运行时库以及富有表现力的面向对象结构,这些功能可以帮助您快速开发可靠的代码。
Java 的本机代码编译器的功能正如其名称所示:将您的 Java 源代码编译为目标机器代码。这意味着您不必在目标上安装 JVM(Java 虚拟机)。当您运行程序的代码时,它不会启动 VM,它只会像任何其他程序一样加载和运行。这并不一定意味着您的代码会运行得更快。有时,在热点 VM 上运行的字节码与 GCJ 编译的代码相比,性能数字会更好。
使用 GCJ 的一个优点是您无需 JVM 即可节省空间。您还可以节省版税。此外,使用 GCJ 可以让您使用所有开源软件交付解决方案,这通常是一件好事。
嵌入式工程师在为目标创建根文件系统时首先想到的是值得信赖的 uClibc,它是 glibc 库的紧凑实现。对于那些刚开始在嵌入式目标上使用 Linux 的人来说,标准的 C 库在处理可能只有 8MB(例如)用于根文件系统的目标时可能会显得有点大。为了节省嵌入式系统根文件系统上的空间,工程师会将标准 C 库切换到更小的库,例如 uClibc。GCJ 需要 unicode 支持,而 uClibc 不支持 unicode,因此 glibc 是必需的。
GCJ 的标准库大小为 16MB,因此即使您可以通过切换到更小的标准 C 库来节省空间,但总体而言差异也不会太大。可以通过删除对执行 Java 字节码的支持来修剪标准 GCJ 库,但是失去该功能会降低 GCJ 的整体价值。
由于本文是关于在嵌入式环境中使用 GCJ,因此它向您展示了如何构建交叉编译器和目标系统的简单根文件系统。对于那些刚接触嵌入式开发的人来说,交叉编译器构建的代码在与编译发生的机器不同的处理器上运行。运行编译器的机器称为主机,代码运行的机器称为目标。
在本文中,目标系统是基于 PPC 745/755 的系统,运行频率为 350MHz。这款特殊的板卡装在一个半透明的外壳中,配有显示器和硬盘驱动器,也被称为 iMac。好吧,这很难说是嵌入式系统的典型示例,但它确实带来了一些您在真正的嵌入式系统中会遇到的相同挑战。您在这里学到的东西应该很好地适用于使用其他处理器的嵌入式系统。
主机系统是一台普通的 IBM ThinkPad 笔记本电脑,运行 Pentium III 处理器。Yellow Dog Linux 已经在主机系统上运行,但是通过一些小技巧,我们将使其使用本文中创建的根文件系统进行测试。
首先,我们需要一个在我们的 Pentium 机器上运行的交叉编译器,该编译器为基于 PowerPC 750 的处理器创建代码。为目标系统构建交叉编译器可能是一个非常繁琐的过程;一个可用的编译器不仅仅是 GCC,它还包含一些额外的工具(亲切地称为 binutils)和该语言的标准库。
为了快速启动并运行交叉编译器,请尝试使用 crosstool 包,感谢 Dan Kegel。Crosstool 完成了构建交叉编译器所需的所有艰苦工作:它获取源代码和补丁,应用补丁,配置软件包并启动构建。在获取并解压缩 crosstool 后,以下是构建 GCJ 交叉编译器的步骤
$ export TARBALLS_DIR=~/crosstool-download $ export RESULT_TOP=/opt/crosstool $ export GCC_LANGUAGES="c,c++,Java" $ eval `cat powerpc-750.dat gcc-4.0.1-glibc-2.2.2.dat' sh.all --notest
在等待编译完成时,让我们看看我们刚才做了什么来启动我们的 crosstool 构建。TARBALLS_DIR 是 crosstool 下载其文件的位置。Crosstool 默认获取构建所需的所有文件。RESULT_TOP 是交叉编译器的安装目录。最后,GCC_LANGUAGES 控制将为编译器启用哪些语言前端。GCC 支持许多不同的语言前端,每个前端都会大大增加编译过程的时间,因此此工具链构建仅选择了必要的语言前端。
对于那些没有 bash 脚本-foo 许可证的人来说,最后一行将两个 dat 文件转储到命令行,并使用参数 --notest 执行 all.sh 脚本。为了简化交叉编译器的构建,crosstool 包含配置文件,其中为目标处理器和 gcc/glibc 组合设置了正确的环境变量。在本例中,crosstool 构建了一个 gcc 4.0.1,其中 glibc 2.2.2 针对 PPC 750 处理器。Crosstool 包含适用于所有主要处理器架构和 glib/gcc 组合的脚本。
构建完成后,交叉编译器将安装在 $RESULT_TOP/gcc-4.0.1-glibc-2.2.2/powerpc-750-linux-gnu/bin 中。将其添加到您的路径中,以使调用交叉编译器更容易。
获取并解压缩 Crosstool
Crosstool 是 Dan Kegel 的作品。您可以通过访问 kegel.com/crosstool 了解您想了解的关于 crosstool 的一切。该页面有一个很棒的快速入门指南以及完整的文档。本文使用了版本 0.38,可在 kegel.com/crosstool/crosstool-0.38.tar.gz 获取。
在 crosstool 主页上,查看 buildlogs 链接 (kegel.com/crosstool/crosstool-0.38/buildlogs) 以查看哪些 glibc/gcc 组合为您的目标架构成功构建。
使用您新近构建的交叉编译器编译的第一件事是根文件系统。在这种情况下,根文件系统由 BusyBox 提供。对于新手来说,BusyBox 是一个单二进制文件,它在一个非常小的可执行文件中封装了最流行的 UNIX 实用程序的迷你版本。BusyBox 为那些计算字节数的人而构建,有数百个旋钮可供调整,以创建具有您所需实用程序的根文件系统,并在您期望的空间限制内。为了本文的目的,我们更改了 BusyBox 配置,使其可以交叉编译,并将大小优化留给读者作为练习。
BusyBox 是嵌入式 Linux 世界的中流砥柱,由 Erik Anderson 维护。获取 BusyBox 的一种方法是从 www.busybox.net/downloads/busybox-1.01.tar.bz2 下载。
要创建 BusyBox 根文件系统,您需要调用make menuconfig在 BusyBox 解压缩的目录中。menuconfig 程序的工作方式与 2.4/2.6 menuconfig 内核配置界面完全相同。以下是构建根文件系统需要执行的操作。
首先,选择构建选项。选中“您是否要使用交叉编译器构建 BusyBox?”框。在单击此选项时出现的输入控件中填写交叉编译器的前缀,在本例中为 powerpc-750-linux-gnu-。BusyBox 构建脚本在编译期间连接必要的工具名称(gcc、ld 等)。确保编译器在您的 $PATH 中。
接下来,运行 make 和 install
make make install
BusyBox 将新构建的根文件系统放在 ./_install 中。您会注意到 BusyBox 的编译时间比 GCC 短得多。
快完成了!BusyBox 创建的根文件系统不包含任何库。GCJ 程序需要一些库,BusyBox 也需要一些库,如表 1 所示。
表 1. GCJ 和 BusyBox 所需的库
库文件 | 功能 |
---|---|
ld.so.1 | 动态链接文件加载器。在程序运行时调用,加载所需的库并执行动态链接。 |
libdl.so.2 | 用于操作动态库的辅助函数。 |
libgcc_s.so.1 | 定义处理异常的接口。 |
libgcj.so.6 | GCJ 运行时库。包含标准 Java 库的实现。 |
libm.so.6 | 数学函数库。 |
libpthread.so.0 | POSIX 线程库。 |
这些库与交叉编译器使用的库相匹配。在本例中,这些文件存储在 $RESULT_TOP/gcc-4.0.1-glibc-2.2.2/powerpc-750-linux-gnu/powerpc-750-linux-gnu/lib 目录中(不是拼写错误!)。将它们放入根文件系统的最简单方法是直接复制它们
for f in ld.so.1 lib libdl.so.2 libgcc_s.so.1libgcj.so.6 libm.so.6 libpthread.so.0 ; do cp $RESULT_TOP/gcc-4.0.1-glibc-2.2.2/powerpc-750-linux-gnu/powerpc-750-linux-gnu/lib/$f <busybox install directory>/lib $RESULT_TOP/gcc-4.0.1-glibc-2.2.2/powerpc-750-linux-gnu/bin/power pc-750-linux-gnu-strip <busybox install directory>/lib/$f done
您还需要在根文件系统中创建一个文件夹 /proc,用作 proc 文件系统的挂载点。细心的人会注意到,我没有保留用于容纳不同库版本的符号链接——这在嵌入式系统中是一种典型的快捷方式,在嵌入式系统中,与桌面系统不同,库配置在设备的整个生命周期内不会改变。运行 strip 可以大大减少文件所需的磁盘空间,几乎减少 50%。
此时,可以将根文件系统复制到目标系统的 /tmp/bbox 目录中。要告诉系统使用此作为根文件系统,请以 root 身份启动终端并执行 chroot 命令
chroot /tmp/bbox /bin/ash
此命令将 / 挂载点重新映射到 /tmp/busybox,并运行 /bin/ash 以获取终端。它工作了吗?恭喜!您已从头开始为嵌入式系统创建了一个完整的根文件系统。拍拍自己的背。
GCJ 也需要挂载 proc 文件系统。在 chroot 之后,您需要通过执行以下操作将 proc 文件系统重新挂载到当前根文件系统中
mkdir /proc mount -t proc none /proc
尽管此根文件系统驻留在标准驱动器上,但部署在生产嵌入式系统上的根文件系统不会有太大差异。唯一必要的更改是创建 inittab,以便板卡在启动时运行正确的脚本,并添加一个 /dev 文件系统,其中包含目标板卡的正确设备文件。
在构建交叉编译器和根文件系统之后,构建您的 GCJ 应用程序将有点平淡无奇。我们将从传统的 hello world 开始
Class hello { Static public void main(String argc[]) { System.out.println("hello from GCJ"); } }
按照 Java 约定,此类驻留在 hello.class 文件中。要编译该文件,请输入
powerpc-750-linux-gnu-gcj hello.class --main=hello -o hello-java
--main=hello 发生了什么?任何类都可以定义一个具有合适入口点的方法。--main=hello 选项告诉链接器在链接时使用 hello 类中的 main 方法。省略此选项会导致链接错误“undefined reference to main”,对于新手来说,这会令人困惑,因为您的类包含 main。
将此文件下载到目标并在 chroot shell 中运行它。您将看到
# ./java-test Hello from GCJ
此时,开发与任何其他 Java 项目非常相似地进行,除了调用 GCJ 交叉编译器而不是本机 javac 编译器。
在此示例中,根文件系统的大小超过 20MB。由于许多嵌入式系统使用闪存,其每兆字节成本比基于磁盘的存储系统昂贵得多,因此最小尺寸的根文件系统通常是一项重要的业务需求。减少根文件系统大小的一种简单方法是静态链接您的应用程序。尽管乍一看这似乎违反直觉,因为您的应用程序中将有一个额外的 libc 代码副本,但请回想一下 libgcj.so 包含整个 Java 标准库。大多数应用程序仅使用 Java 标准库的一小部分,因此使用静态链接是剔除库中未使用代码的好方法。只需确保剥离生成的二进制文件;否则,您会因 libgcj.so 中调试信息的数量而感到震惊。
Gene Sally 在过去十年中一直以某种形式从事 Linux 工作。如今,Gene 将他的注意力集中在帮助工程师在嵌入式目标上使用 Linux。欢迎通过 gene.sally@gmail.com 联系 Gene。