交叉编译指在一个平台上生成另一个平台可执行的代码,这里的平台包括体系结构(Architecture)和操作系统(Operating System)。同一个体系结构可以运行不同的操作系统,且同一个操作系统也可以在不同体系结构上运行。本文从两个方面讲解如何搭建Linux交叉开发环境。 在两种情况下,通常需要交叉编译:第一,在项目的起始阶段,目标平台还没有建立,此时需要进行交叉编译,以生成所需的bootloader(启动引导代码)以及操作系统核心;第二,当目标平台能启动后,由于目标平台上的资源有限,当编译大型程序时,有可能也需要交叉编译。
一、建立交叉编译开发工具链
建立交叉编译开发工具链有两种方法:手动编译一个工具链和直接下载制作好的工具链。
1.1 编译工具链
在进行嵌入式开发前,需要建立一套由编译器、连接器和libc库等构成的交叉编译环境。下面以建立针对ARM的交叉编译开发环境为例来讲解,注意本文采用的开发环境式宿主机为i386-redhat-9.0、目标机为ARM。 整个编译过程包括:(1)下载源文件、补丁和建立编译的目录;(2)建立内核头文件;(3)建立二进制工具(binutils);(4)建立初始编译器(bootstrap gcc);(5)建立C库(glibc);(6)建立全套编译器(full gcc)。
1.1.1 下载源文件、补丁和建立编译的目录
(1)选定软件的版本号 。先查看glibc代码中的INSTALL文件,该文件中列举了本版本的glibc编译时所需要的binutils和gcc的版本号。本文采用的软件本文为:linux-2.4.21+rmk2;gcc-2.95.3;glibc-2.2.3;glibc-linuxthreads-2.2.3。Linux内核可以从www.kernel.org网站上下载。binutils、gcc和glibc可以从FSF的FTP站点(http://ftp.gnu.org/gnu/)下载。在编译glibc时,需要到Linux内核中的include目录的内核头文件。gcc推荐使用gcc-2.95以上版本,太老的版本在编译时可能出现问题,gcc-2.95.3是一个比较稳定的版本,也是内核开发人员推荐使用的。另外,太新的版本大多数没有经过大量的测试,建议不要选用。 (2)建立几个用来工作的目录 。假设用户目录为/home/liang,则建立一个项目目录embedded: $pwd
/home/liang
$mkdir embedded
然后,在这个项目目录embedded下建立3个子目录build-tools、kernel和tools。 build-tools用来存放下载的binutils、gcc和glibc的源代码以及编译这些源代码的目录。 kernel用来存放内核代码和内核补丁。 tools用来存放编译好的交叉编译工具和库文件,指令如下: $cd /home/liang/embedded
$mkdir build-tools kernel tools
执行完上述命令后,目录结构如下: $ls embedded
build-tools kernel tools
紧接着,需要输出环境变量,输入如下环境变量以便编译: $export PRJROOT=/home/liang/embedded
$export TARGET=arm-linux
$export PREFIX=$PRJROOT/tools
$export TARGET_PREFIX=$PREFIX/$TARGET
$export PATH=$PREFIX/bin:$PATH
如果不想用环境变量,也可以直接采用绝对路径。环境变量也可以定义在.bashrc文件中,这样当logout或换了控制台时,就不用总是export这些变量了。 体系结构和TARGET变量对应值下表所示:
体系结构 TARGET PowerPC Powerpc-linux ARM arm-linux MIPS(big endian) mips-linux MIPS(little endian) mipsel-linux MIPS64 mips64-linux SuperH3 sh3-linux SuperH4 sh4-linux I386 i386-linux Ia64 ia64-linux M68k m68k-linux M88k m88k-linux Alpha alpha-linux Sparc sparc-linux Sparc64 sparc64-linux
可以在通过glibc下的config.sub脚本来之道所选用的TARGET变量是否被支持,例如: $./config.sub arm-linux
上述环境中,config.sub在glibc-2.2.3/scripts目录下。 (3)建立编译目录 。为了把源代码和编译时生成的文件分开,通常编译工作不在源码目录中进行,需要新建一个目录专门用来编译。用如下的命令来建立编译下载的binutils、gcc和glibc的源代码的目录: $cd $PRJROOT/build-tools
$mkdir build-binutils build-boot-gcc build-gcc build-glibc gcc-patch
其中,build-binutils——编译binutils的目录; build-boot-gcc——编译gcc启动部分的目录; build-glibc——编译glibc的目录; build-gcc——编译gcc全部目录; gcc-patch——存放gcc的补丁的目录。 gcc-2.95.3的补丁包括gcc-2.95.3-2.patch、gcc-2.95.3-no-fixinc.patch和gcc-2.95.3-returntype-fix.patch,这些均可以从“http://www.linuxfromscratch.org”下载这些补丁。再将下载的binutils-2.10.1、gcc-2.95.3、glibc-2.2.3和glibc-linuxthreads-2.2.3的源代码放入build-tools目录中。 build-tools目录的内容包含如下: $ls
binutils-2.10.1.tar.bz2 build-gcc gcc-patch
build-binutils build-glibc glibc-2.2.3.tar.gz
build-boot-gcc gcc-2.95.3.tar.gz glibc-linuxthreads-2.2.3.tar.gz
1.1.2 建立内核头文件
(1)把从“www.kernel.org”下载的内核源代码放入到$PRJROOT/kernel目录。进入kernel目录: $cd $PRJROOT /kernel
解压内核源代码: $tar -xzvf linux-2.4.21.tar.gz
或者 $tar -xjvf linux-2.4.21.tar.gz
给Linux内核打上补丁: $cd linux-2.4.21
$patch -p1 < ../patch-2.4.21-rmk2
编译内核生成头文件: $make ARCH=arm CROSS_COMPILE=arm-linux- menuconfig
配置完成后保存并退出,检查内核目录中是否生成include/linux/version.h和/include/linux/autoconf.h文件,它们两个是编译glibc时需要用到的,如果这两个文件存在就说明生成了正确的头文件。 (2)紧接着,需要建立几个正确的链接: $cd include
$ln -s asm-arm asm
$cd asm
$ln -s arch-epxa arch
$ln -s proc-armv proc
执行完,这几条命令后,即可以为交叉编译环境建立内核头文件链接了,代码如下: $mkdir -p $TARGET_PREFIX/include
$ln -s $PRJROOT/kernel/linux-2.4.21/include/linux $TARGET_PREFIX/include/linux
$ln -s $PRJROOT/kernel/linux-2.4.21/include/asm-arm
$TARGET_PREFIX/include/asm
1.1.3 建立二进制工具(binutils)
binutils是一些二进制工具的集合,其中包含了一些常用的as和ld。 解压下载的binutils源文件: $cd $PRJROOT/build-tools
$tar -xjvf binutils-2.10.1.tar.bz2
然后进入build-binutils目录配置和编译binutils: $cd build-binutils
$../binutils-2.10.1/configure --target=$TARGET --prefix=$PREFIX
“–target”选项表示生成的是arm-linux的工具,“–prefix”表示可执行文件的安装位置。此时会出现很多check,最后生成Makefile文件。有了Makefile文件后,需要进一步编译并安装binutils,如下所示: $make
$make install
此时,$PREFIX/bin中生成的文件如下: $ls $PREFIX/bin
arm-linux-addr2line arm-linux-objecopy arm-linux-ar arm-linux-objdump arm-linux-as arm-linux-ranlib arm-linux-c++filt arm-linux-readelf arm-linux-gasp arm-linux-size arm-linux-ld arm-linux-strings arm-linux-nm arm-linux-strip
其中,addr2line——将要照的地址转换为文件和行号,它要使用debug信息; objcopy——将某种格式的目标文件转换为另一种格式的目标文件; ar——生成、修改和解压一个存档文件; objdump——显示目标文件的信息; as——gnu的汇编器; ranlib——为一个存档文件生成一个索引,并将这些索引存入存档文件中; c++filt——C++和java中由一种重载函数,所用的重载函数最后会被编译转换为汇编的标号,c++filt就是实现这种反向的转换,并根据标号得到函数名; readelf——显示elf格式的目标文件的信息; gasp——gnu汇编器的预编译器; size——显示目标文件各个节与目标文件的大小; ld——gnu的连接器; strings——打印出目标文件中可以打印的字符串,默认长度为4; nm——罗列目标文件的符号与对应地址; strip——去掉目标文件的所有符号信息。
1.1.4 建立初始编译器(bootstrap gcc)
注意,在编译和安装gcc之前,还需要修改一个文件“$PRJROOT/gcc/config/arm/t-linux”,把“TARGET_LIBGCC2-CFLAGS=-fomit-frame-pointer-fPIC”这一行修改成: TARGET_LIBGCC2-CFLAGS=-fomit-frame-pointer-fPIC-Dinhibit_libc-D__gthr_posix_h。 (1)进入build-tools目录,下载gcc源代码并解压: $cd $PRJROOT/build-tools
$tar -xvzf gcc-2.95.3.tar.gz
(2)进入gcc-2.95.3目录,给gcc打上补丁: $cd gcc-2.95.3
$patch -pl< ../gcc-patch/gcc-2.95.3.-2.patch
$patch -pl< ../gcc-patch/gcc-2.95.3.-no-fixinc.patch
$patch -pl< ../gcc-patch/gcc-2.95.3-returntype-fix.patch
$echo timestamp > gcc/cstamp-h.in
(3)配置bootstrap gcc,后续采用bootstrap gcc来编译glibc库: $cd ..
$ cd build-boot-gcc
$../gcc-2.95.3/configure --target=$TARGET --prefix=$PREFIX >--without-headers --enable-languages=c --disable-threads
上条命令中的“–target”、“–prefix”与配置binutils的含义是相同的;“–without-headers”表示不需要头文件,这是因为采用的是交叉编译工具,因此不需要本机上的头文件。“–enable-languages=c”表示boot-gcc只支持C语言;“–disable-threads”表示去掉thread功能,该功能需要glibc的支持。 (4)编译和安装boot-gcc $make all-gcc
$make install-gcc
下面罗列出$PREFIX/bin中多出来的文件: $ls $PREFIX/bin
arm-linux-gcc arm-linux-unprotoize cpp gcov
其中,gcc——gnu的C语言编译器; unprotoize——将ANSI C的代码转换为K&R C的形式,即去掉函数原型的参数类型; cpp——gnu的C语言预编译器; gcov——gcc的辅助测试工具,可以用来分析和优化程序。 注意!在使用gcc3.2以上版本(包括自己)时,配置boot-gcc时不能使用–without-headers选项,因为需要使用glibc的头文件。
1.1.5 建立C库(glibc)
(1)解压glibc-2.2.3.tar.gz和glibc-linuxthreads-2.2.3.tar.gz的源代码: $cd $PRJROOT/build-tools
$tar -xvzf glibc-2.2..3.tar.gz
$tar -xvzf glibc-linuxthreads-2.2.3.tar.gz --directory=glibc-2.2.3
(2)进入build-glibc目录配置glibc: $cd build-glibc
$CC=arm-linux-gcc ../glibc-2.2.3/configure --host=$TARGET --prefix="/usr" --enable-add-ons --with-headers=$TARGET_PREFIX/include
“CC=arm-linux-gcc”是把CC变量设置为编译后的bootstrap gcc,用它来编译glibc;“–enable-add-ons”表示glibc采用放入glibc源码目录中的linuxthreads包,该命令等价于“-enable-add-ons=linuxthreads”;“–with-headers”表示glibc linux内核头文件的目录位置。 (3)编译和安装glibc: $make
$make install_root=$TARGET_PREFIX prefix="" install
注意,此时需要修改libc.so文件,将GROUP(/lib/libc.so.6/lib/libc_nonshared.a)修改为GROUP(libc.so.6 libc_nonshared.a)。
1.1.6 建立全套编译器(full gcc)
(1)建立全套编辑器,此处需要支持C和C++语言: $cd $PRJROOT/build-tools/build-gcc
$../gcc-2.95.3/configure --target=$TARGET --prefix=$PREFIX --enable-languages=c,c++
“–enable-languages=c,c++”表示full gcc支持C和C++语言。 (2)编译和安装full gcc: $make all
$make install
查看$PREFIX/bin中多了那些文件: $ls $PREFIX/bin
arm-linux-g++ arm-linux-protoize arm-linux-c++
其中,g++——gnu的C++编译器; protoize——与unprotoize相反,将K&R C的源码转换为ANSI C的形式,在函数原型中加入参数类型; c++——gnu的C++编译器。 至此,交叉编译工具已经编译完成了。
1.2 下载工具链
通常,编译一个交叉工具链非常复杂,而且很容出错。因此,直接下载一个编译好的工具链是非常实用的。本文介绍Denx公司发布的一套开源的嵌入式Linux开发工具链ELDK(Embedded Linux Development Kit,ELDK),ELDK不仅简单而且非常好用,最重要的是它还是完全免费的!ELDK的下载网址如下表所示:
HTTP FTP http://mirror.switch.ch/ftp/mirror/eldk/eldk/ ftp://mirror.switch.ch/mirror/eldk/eldk/ http://sunsite.utk.edu/ftp/pub/linux/eldk/ ftp://sunsite.utk.edu/pub/linux/eldk/ http://ftp.sunet.se/pub/Linux/distributions/eldk/ ftp://ftp.sunet.se/pub/Linux/distributions/eldk/ http://archiv.leo.org/pub/comp/os/unix/linux/eldk/ ftp://ftp.leo.org/pub/eldk/
ELDK同时支持ARM、PPC、MIPS等多种处理器,它包括一个安装工具和很多RPM包,它可以安装到用户的交叉编译的主机中。ELDK的RPM包分为两类:(1)嵌入式Linux开发工具;(2)为目标处理器编译号的工具和元件。其中,第一部分执行在开发主机上的交叉编译工具,包括gnu交叉编译器gcc、binutils和gdb等;第二部分是一些已经编译好的、能在目标开发板上运行的工具和库,这些工具和库可以用来建立一个Linux的Roofs文件系统。ELDK中的元件包如下表所示:
名称 版本 名称 版本 binutils 2.14 make 3.79.1-17 cpp 3.3.3 make-doc 3.79.1-17 gcc 3.3.3 mkcramfs 0.0.1 gcc-c++ 3.3.3 mkimage 1.1.3 gdb 5.2.1-4b mtd_utils 2-1 genext2fs 1.3 rpm 4.1.1-1.8xa ldd 0.1 rpm-build 4.1.1-1.8xa
如果需要安装Linux开发主机版本的ELDK,可以执行如下命令: $chmod+x install
./install [-d <dir>] [<cpu_family1>] [<cpu_family2>] ...
其中,(1)[ -d < dir >]表示安装的目录,如果不设置则表示安装在当前目录; (2)<cpu_family>:表示需要安装的目录CPU的类型。 可将ELDK安装到任何目录,注意需要具有写和执行的权限,并不需要由root权限。 ELDK是基于RPM结构的,表明每个ELDK的元件都是一个个的RPM文件,它们可以安装或者删除。 (1)罗列出已经安装的ELDK元件包的指令为: ${CROSS_COMPILE}rpm -aq
(2)删除一个元件包的指令为: ${CROSS_COMPILE}rpm -e <package_name>
(3)安装一个元件包指令为: ${CROSS_COMPILE}rpm -i <package_file_name>
(4)更新一个元件包的指令为: ${CROSS_COMPILE}rpm -U <package_file_name>
如果要删除ELDK,则只需要删除ELDK安装的目录即可: $rm -rf <dir>
其中,< dir >表示ELDK安装的目录。 注意,在执行上述命令前一定要配置好正确的ELDK环境变量。设置CROSS_COMPILE的环境变量,并添加ELDK的bin和usr/bin路径到系统的PATH环境变量中。CROSS_COMPILE将区分x86主机的gcc工具和安装的ELDK的ARM的gcc交叉编译工具。下面为一个安装并配置ELDK的例子: (1)建立ELDK的安装目录: $mkdir /opt/eldk
(2)将安装程序载入光盘: $mount /dev/cdrom /mnt/cdrom
(3)运行install脚本,安装ELDK到指定的目录: $/mnt/cdrom/install -d /opt/eldk
(4)安装完成后,输出CROSS_COMPILE环境变量: $export CROSS_COMPILE=arm-linux-
(5)在PATH环境变量中加入/opt/eldk/usr/bin和/opt/eldk/bin目录: $PATH=$PATH:/opt/eldk/usr/bin:/opt/eldk/bin
1.3 验证工具链
安装了交叉编译工具链后,需要经过验证才能正式实用。下面是一个简单的程序“helloworld.c”:
#include <stdio.h>
int main ( void )
{
printf ( "hello world!\n" ) ;
return 0 ;
}
对于1.1节搭建好的交叉工具链进行编译,结果如下: $arm-linux-gcc -o helloworld helloworld.c
$file helloworld
helloworld:ELF 32-bit LSB executable, ARM, version 1, dynamically linked (uses shared libs), not stripped
上面的输出说明编译成功了一个能在ARM体系结构上运行“helloworld.c”,这就证明工具链已经搭建成功了。 对于1.2节搭建好的工具链,可以进行一下编译进行验证: ${CROSS_COMPILE}gcc -o helloworld helloworld.c
$file helloworld
helloworld:ELF 32-bit LSB executable, ARM, version 1, dynamically linked (uses shared libs), not stripped
上面的输出证明ELDK安装成功。
二、配置主机服务
在嵌入式软件开发过程中,有些主机服务有时非常必要,这样会为开发工作开来便利,下面主要介绍4种主机服务:samba、DHCP、TFTP和NFS。
2.1 配置samba
samba服务能够实现Linux和Windows之间的文件共享,方便于文件编辑,具体配置方法如下: (1)修改samba配置文件“/etc/samba/smb.conf”:第一,修改interfaces=your ip/24;第二,添加开放的目录和权限,如下:
[sharename]
comment = Insert a comment here
path = /home/share/
valid users = root
browseable = yes
writable = yes
printable = no
create mode = 0664
directory mode = 0775
(2)为samba创建一个单独的口令文件,根据“/etc/passwd”文件来创建,在shell种输入命令: $cat /etc/passwd | mksmbpasswd.sh > /etc/samba/smbpasswd
(3)改变samba口令文件的权限,只有根用户才有读写权限: $chmod 600 /etc/samba/smbpasswd
(4)设置每个samba用户的口令,如下命令把username替换为每个用户的用户名: $smbpasswd username
(5)加密口令必须在samba配置文件种被启用,在“smb.conf”文件种,请确定一下行没有被注释掉:
encrypt passwords = yes
smb passwd file = /etc/samba/smbpasswd
(6)在shell命令行输入“service smb restart”来确定samba服务被启动。 (7)为了避免每次启动都要手动启动samba服务,可使用如下命令在系统启动时自动启动samba服务: $chkconfig smb on
注意,使用samba服务之前,必须关闭Linux的防火墙功能。
2.2 配置DHCP
DHCP是动态主机配置协议,这个协议用于向计算机自动提供IP地址,子网掩码和路由信息。在开发过程中,目标系统没有自己的静态IP地址,在启动时向DHCP服务器申请,因此需要在主机上配置DHCP服务,使得目标系统在请求IP地址时,动态地为它分配IP地址。 DHCP服务地配置文件为“etc/dhcpd.conf”,它通常包含三部分:parameters、declarations和option。 (1)DHCP配置文件地parameters(参数)表示是否和如何执行任务,即将那些网络配置选项发送给用户,parameters的主要内容如下表所示:
参数 含义 ddns-update-style 配置DHCP-DNS互动更新模式 default-lease-time 指定缺省租赁时间的长度(s) max-lease-time 指定最大租赁时间的长度(s) hardware 指定网卡接口类型和MAC地址 sever-name 通知DHCP客户服务器名称 get-lease-hostnames flag 检查客户端使用的IP地址 fixed-address ip 分配给客户端一个固定的地址 authritative 拒绝不正确的IP地址请求
(2)DHCP配置文件中的declarations(声明),用来描述网络布局、提供用户的IP地址等,declarations的主要内容如下表所示:
声明 解释 shared-network 用来告知是否一些子网络分享相同的网络 subnet 描述一个IP地址是否属于该子网 range 起始IP终止IP 提供动态分配IP地址的范围 host主机名称 参考特别的主机 group 为一组参数提供声明 allow unknown-clients; deny unknown-client 是否动态分配IP地址给未知的用户 allow bootp; deny bootp 是否响应激活查询 allow booting; deny booting 是否响应用户查询 filename 开始启动文件的名称,应用于无盘工作站 next-server 设置服务器从引导文件中装入主机名,应用于无盘工作站
(3)DHCP配置文件中的option(选项),用来配置DHCP可选参数,其主要内容如下表所示:
选项 解释 subnet-mask 为用户端设置子网掩码 domain-name 为用户端指明DNS名字 domain-name-servers 为用户端知名DNS服务器的IP地址 host-name 为用户端指定主机名称 routers 为用户端设定默认网关 broadcast-address 为用户端设定广播地址 ntp-server 为用户端设定网络时间服务器的IP地址 time-offset 为用户端设定与格林尼治时间的偏移时间(s)
ddns- update- style interim;
ignore client- updates;
allow bootp;
subnet 192.168 .1 .0 netmask 255.255 .255 .0 {
range dynamic- bootp 192.168 .1 .1 192.168 .1 .200 ;
default - lease- time 216000 ;
max- lease- time 432000 ;
# we want the nameserver to appear at a fixed address
host armlinux{
hardware ethernet 11 : 22 : 33 : 44 : 55 : 66 ;
fixed- address 192.168 .1 .20 ;
option root- path "/tftpboot" ;
}
}
重启DHCP服务,命令如下: $service dhcpd restart
为了避免每次启动主机都要手动启动DHCP服务,使用如下命令使得DHCP服务在每次系统启动时都默认执行: $chkconfig dhcpd on
2.3 配置TFTP
在目标系统的开发过程中,Linux内核是从主机下载到目标系统上解压并运行的,因此主机必须提供这种文件传输功能。TFTP是一种简单的文件传输协议,多用于嵌入式系统应用中,因此主机需要配置TFTP服务,以供内核下载时使用。 (1)修改而皮质参数,TFTP服务的配置文件为“etc/xinetd.d/tftp”,只需要将server-args的参数设置为“-s/tftpboot”,/tftpboot为包含要下载到目标系统中去的Linux内核映射文件的目录,其他使用默认设置即可。 (2)在主机上创建/tftpboot目录,用于存放内核,命令如下: $mkdir /tftpboot
(3)重启TFTP服务: $service xinetd restart
(4)为了避免每次重启主机都要手动启动TFTP服务,使用如下指令实现TFTP服务在每次启动系统时都默认执行: $chkconfig tftp on
典型的TFTP配置文件如下:
service tftp
{
disable=no
socket-type=dgram
protocol=udp
wait=yes
user=root
server=/usr/sbin/in.tftpd
server-args=-s /tftpboot
per_source=11
cps=100 2
flags=IPv4
}
2.4 配置NFS
网络文件系统(NFS)是一种在网络上及其间共享文件的方法,在开发过程中,目标系统没有足够的本地存储设备,可以通过主机提供NFS服务,使用在主机上的文件系统,就如同本地硬盘驱动器一样。 (1)修改NFS配置文件 NFS服务的配置文件为“etc/exports”,其格式如下: 共享目录 主机名1或IP1(参数1,参数2) 主机名2或IP2(参数3,参数4)
上面的格式表示:一个共享目录提供给两台不同的主机,但提供给这两台主机的权限和参数可以是不同的,可以设置的参数如下所示: rw:可读写的权限; ro:只读权限; root_squash:root用户的所有请求映射为如匿名(anonymous)用户一样的权限; no_root_squash:保留共享文件的UID和GID; all_squash:共享文件的UID和GID映射匿名用户,适用于公共目录; no_all_squash:保留共享文件的UID和GID; sync:资料同步写入到内存和硬盘中; async:资料会先暂存于内存中,而非直接写入硬盘; secure:NFS通过1024个以下的安全TCP/IP端口发送; insecure:NFS通过1024个以上的端口发送; hide:在NFS共享目录不共享其子目录; no_hide:共享NFS目录的子目录。 (2)重启NFS服务: $service nfs restart
(3)为了避免每次启动时手动启动NFS服务,输入如下指令在启动机器时自动启动NFS: $chkconfig nfs on
典型的NFS配置文件如下: /tftpboot *(rw, no_root_squash, no_all_squash)