第3章 嵌入式Linux程序设计基础
Linux操作系统虽然在嵌入式设备上得到了广泛运用,但是,Linux操作系统最初并不是为嵌入式设备所设计的,因此,要在相应的嵌入式设备中运行Linux操作系统,需要建立适合相关应用环境的交叉编译环境。本章将介绍嵌入式Linux开发环境及程序设计基础。
3.1 建立嵌入式Linux交叉编译环境
3.1.1 编译环境概述
因为应用的硬件环境不一样,在台式机上用X86的GCC编译的可执行文件是不能在嵌入式ARM处理器上运行的,因为X86处理器和ARM处理器是不同的硬件平台,即硬件体系结构存在差异。但是,由于目前嵌入式设备本身资源的特点,应用于ARM处理器上的程序又不得不借助 X86 的台式机进行编辑和编译工作。因此,在 X86 的台式机上编译能够在ARM处理器上运行的应用程序就不能用于X86处理器的GCC,而是需要一组特殊的编译环境,这就是本章要讨论的交叉编译环境。
GCC工具链是目前最受欢迎,应用最为广泛的交叉编译环境,支持绝大多数嵌入式处理器,包括ARM、MIPS、SuperH、PowerPC以及X86。目前,网络已经有很多成功建立交叉编译环境的方式。以下详细介绍这一组工具特点。
1.Binutils二进制工具包
Binutils二进制工具包是一组二进制处理工具的集合,主要工具有以下两种。
● ld:链接器。
● as:GNU汇编器。
除此之外,还包括以下工具。
● addr2line:把程序地址转换为文件名和行号,在命令行中给出一个地址和可执行文件,其就会使用这个可执行文件的调试信息给出的地址上的文件及行号。
● ar:建立、修改以及提取归档文件。
● c++filt:连接器使用它来过滤C++符号。
● gprof:显示程序调用段的信息。
● nlmconv:转换目标文件为NLM。
● nm:列出目标文件中的符号。
● objcopy:复制目标文件。
● objdump:显示一个或多个目标文件信息。
● ranlib:产生归档文件索引,并将其保存到这个归档文件中。
● readelf:显示elf格式可执行文件信息。
● size:列出目标文件每一段的大小及总体大小。
● strings:打印某个文件的可打印字符串。
● strip:丢弃目标文件中的全部或者部分符号。
● windres:一个Windows资源的编译器。
2.GCC交叉编译器
GCC工具是编译程序的最为重要的工具,其包括以下几个主要工具。
● Cpp:C预处理器。
● g++:C++编译器。
● gcc:C编译器。
● gccbug:创建bug报告的Shell脚本。
● gcov:分析在程序中哪里做优化效果最好。
● libgcc*:gcc的运行库。
● libstdc++:标准C++库,包含许多常用的函数。
● libsupc++:提供支持C++语言的库函数。
如果是专门用于ARM处理器,这些工具名称前面一般会加上armv4l-unknown-linux-,即分别为armv4l-unknown-linux-addr2line、armv4l-unknown-linux-g++、armv4l-unknown-linux-objcopy、armv4l-unknown-linux-size、armv4l-unknown-linux-ar、armv4l-unknown-linux-gasp、armv4l-unknown-linux-objdump、armv4l-unknown-linux-strings、armv4l-unknown-linux-as、armv4l-unknown-linux-gcc、armv4l-unknown-linux-protoize、armv4l-unknown-linux-strip、armv4l-unknown-linux-c++、armv4l-unknown-linux-gdb、armv4l-unknown-linux-ranlib、armv4l-unknown-linux-unprotoize、armv4l-unknown-linux-c++filt、armv4l-unknown-linux-ld、armv4l-unknown-linux-readelf、armv4l-unknown-linux-cpp、armv4l-unknown-linux-nm、armv4l-unknown-linux-run。
3.Glibc库说明
Glibc库提供了系统调用和基本函数的C库,所有动态链接的程序都要用到它。相关版本可下载的地址如下。
● Glibc (2.3.2): ftp://ftp.gnu.org/gnu/glibc/
● Glibc-linuxthreads (2.3.2): ftp://ftp.gnu.org/gnu/glibc/
● GlibcSscanfPatch:http://www.linuxfromscratch.org/patches/lfs/cvs/glibc-2.3.2-sscanf-1.patch
Glibc中主要有下列程序。
catchsegv:当程序发生segmentation fault的时候,用来建立一个堆栈跟踪。
gencat:建立消息列表。
getconf:针对文件系统的指定变量显示其系统设置值。
getent:从系统管理数据库获取一个条目。
glibcbug:建立glibc的bug报告并且E-mail到bug报告的邮件地址。
iconv:转化字符集。
iconvconfig:建立快速读取的iconv模块所使用的设置文件。
ldconfig:设置动态链接库的实时绑定。
ldd:列出每个程序或者命令需要的共享库。
lddlibc4:辅助ldd操作目标文件。
locale:是一个Perl程序,可以告诉编译器打开或关闭内建的locale支持。
localedef:编译locale标准。
nscd:提供对常用名称设备调用的缓存的守护进程。
nscd_nischeck:检查在进行NIS+侦查时是否需要安全模式。
pcprofiledump:打印PC profiling产生的信息。
pt_chown:是一个辅助程序,帮助grantpt设置子虚拟终端的属主、用户组和读写权限。
rpcgen:产生实现RPC协议的C代码。
rpcinfo:对RPC服务器产生一个RPC呼叫。
sln:用来创建符号链接,由于它本身是静态链接的,在动态链接不起作用时,sln仍然可以建立符号链接。
sprof:读取并显示共享目标的特征描述数据。
tzselect:对用户提出关于当前位置的问题,并输出时区信息到标准输出。
xtrace:通过打印当前执行的函数跟踪程序执行情况。
zdump:显示时区。
zic:时区编译器。
以下为相关的库文件。
ld.so:帮助动态链接库的执行。
libBrokenLocale:帮助程序处理破损locale,如Mozilla。
libSegFault:处理segmentation fault信号,试图捕捉segfaults。
libanl:异步名称查询库。
libbsd-compat:为了在Linux下执行一些BSD程序,libbsd-compat提供了必要的可移植性。
libc:是主要的C库——常用函数的集成。
libcrypt:加密编码库。
libdl:动态链接接口。
libg g++:g++的运行时。
libieee IEEE:浮点运算库。
libm:数学函数库。
libmcheck:包括了启动时需要的代码。
libmemusage:帮助memusage搜集程序运行时内存占用的信息。
libnsl:网络服务库。
libnss*:名称服务切换库,包含了解释主机名、用户名、组名、别名、服务、协议等的函数。
libpcprofile:帮助内核跟踪在函数、源代码行和命令中CPU使用时间。
libpthread:POSIX线程库。
libresolv:创建、发送及解释到因特网域名服务器的数据包。
librpcsvc:提供RPC的其他服务。
libr:提供了大部分的POSIX.1b实时扩展的接口。
libthread_db:对建立多线程程序的调试很有用。
libutil:包含了在很多不同的UNIX程序中使用的“标准”函数。
3.1.2 建立交叉编译环境流程
在裁剪和定制Linux运用于嵌入式系统之前,由于嵌入式开发系统存储大小有限,通常需要在PC上建立一个用于目标板的交叉编译环境。这是一个由编译器、连接器和解释器组成的综合开发环境。交叉编译工具主要由binutils、gcc和glibc几个部分组成。有时出于对减小libc库大小的考虑,也可以用别的c库来代替glibc,例如uClibc、dietlibc和newlib。建立一个交叉编译工具链是一个比较复杂的过程,如果用户不想自己经历复杂的编译过程,网上有一些编译好的可用的交叉编译工具链可以直接下载。
下面将以建立针对ARM的交叉编译开发环境为例来说明整个过程,其他的体系结构与这个过程相类似,只要做一些对应的改动。这里的开发环境是宿主机i386-redha,目标板为ARM试验箱。这个过程如下:
● 下载源文件、补丁和建立编译的目录;
● 建立内核头文件;
● 建立二进制工具(binutils);
● 建立初始编译器(bootstrap gcc);
● 建立c库(glibc);
● 建立全套编译器(full gcc)。
1.下载源文件、补丁和建立编译的目录
选择软件版本号时,先看看glibc源代码中的INSTALL文件。那里列举了该版本的glibc编译时所需的binutils和gcc的版本号。各个软件的版本及可下载地址为:
binutils-2.14.tar.gz: ftp://ftp.gnu.org/gnu/binutils/binutils-2.14.tar.gz gcc-core-2.95.3.tar.gz: ftp://ftp.gnu.org/gnu/gcc/gcc-2.95.3/gcc-core-2.95.3.tar.gz gcc-g++2.95.3.tar.gz: ftp://ftp.gnu.org/gnu/gcc/gcc-2.95.3/gcc-g++-2.95.3.tar.gz glibc-2.2.4.tar.gz: ftp://ftp.gnu.org/gnu/glibc/glibc-2.2.4.tar.gz glibc-linuxthreads-2.2.4.tar.gz : ftp://ftp.gnu.org/gnu/glibc/glibc-linuxthreads-2.2.4.tar.gz linux-2.4.21.tar.gz: ftp://ftp.kernle.org/pub/linux/kernel/v2.4/linux-2.4.21.tar.gz patch-2.4.21-rmk1.gz: ftp://ftp.arm.linux.org.uk/pub/linux/arm/kernel/v2.4/patch-2.4.21-rmk1.gz
接着建立工作目录:首先建立以下几个用来工作的目录。
在读者的用户目录(编者用的是用户yangzongde,用户目录为 /home/yangzongde)下,先建立一个项目目录embedded。
$pwd /home/yangzongde $mkdir embedded
再在这个项目目录embedded下建立3个目录build-tools、kernel和tools。
● build-tools:用来存放下载的binutils、gcc和glibc源代码和用来编译这些源代码的目录。
● kernel:用来存放内核源代码和内核补丁。
● tools:用来存放编译好的交叉编译工具和库文件。
$cd embedded $mkdir build-tools kernel tools
执行完后目录结构如下:
$ls embedded build-tools kernel tools
接下来就需要输出和环境变量以方便编译。其内容如下:
$export PRJROOT=/home/yangzongde/embedded $export TARGET=arm-linux $export PREFIX=$PRJROOT/tools $export TARGET_PREFIX=$PREFIX/$TARGET $export PATH=$PREFIX/bin:$PATH
如果读者不习惯用环境变量,可以直接用绝对或相对路径。另外,可以通过glibc下的config.sub脚本知道TARGET变量是否被支持,例如:
$./config.sub arm-linux arm-unknown-linux-gnu
接下来建立编译目录:为了将源码和编译时生成的文件分开,一般的编译工作不在源代码目录中,要另建一个目录来专门用于编译。用以下的命令来建立编译下载的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-binutls build-glibc glibc-2.2.3.tar.gz build-boot-gcc gcc-2.95.3.tar.gz glibc-linuxthreads-2.2.3.tar.gz
2.建立内核头文件
把从http://www.kernel.org下载的内核源代码放入$PRJROOT /kernel目录,进入kernel目录。
$cd $PRJROOT /kernel
解开内核源代码
$tar -xzvf linux-2.4.21.tar.gz
或
$tar -xjvf linux-2.4.21.tar.bz2
小于2.4.19的内核版本解开会生成一个Linux目录,若没带版本号,就将其改名。
$mv linux linux-2.4.x
给Linux内核打上补丁
$cd linux-2.4.21 $patch -p1 < ../patch-2.4.21-rmk2
编译内核生成头文件
$make ARCH=arm CROSS_COMPILE=arm-linux- menuconfig
也可以用config和xconfig来代替menuconfig,但这样用可能会没有设置某些配置文件选项和没有生成下面编译所需的头文件。推荐大家用make menuconfig,这也是内核开发人员用得最多的配置方法。配置完退出并保存,检查一下内核目录中的include/linux/version.h和include/linux/autoconf.h文件是不是已经生成,这是编译glibc是要用到的version.h和autoconf.h文件,如果它们存在,也说明生成了正确的头文件。另外,还要建立几个正确的链接。
$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 $in -s $PRJROOT/kernel/linux-2.4.21/include/asm-arm $TARGET_PREFIX/ include/asm
也可以把Linux内核头文件复制过来用。
$mkdir -p $TARGET_PREFIX/include $cp-r$PRJROOT/kernel/linux-2.4.21/include/linux $TARGET_PREFIX/include $cp -r $PRJROOT/kernel/linux-2.4.21/include/asm-arm $TARGET_PREFIX/include
3.建立二进制工具(binutils)
binutils是一些二进制工具的集合,其中包含了常用到的as和ld。首先,解压下载的binutils源文件。
$cd $PRJROOT/build-tools $tar -xvjf 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是指出可执行文件安装的位置。
完成后,生成Makefile文件。有了Makefile后,编译并安装binutils,命令很简单。
$make $make install
下面是$PREFIX/bin下的生成的文件。
$ls $PREFIX/bin arm-linux-addr2line arm-linux-gasp arm-linux-objdump arm-linux-strings arm-linux-ar arm-linux-ld arm-linux-ranlib arm-linux-strip arm-linux-as arm-linux-nm arm-linux-readelf arm-linux-c++filt arm-linux-objcopy arm-linux-size
4.建立初始编译器(bootstrap gcc)
首先进入build-tools目录,将下载gcc源代码解压。
$cd $PRJROOT/build-tools $tar -xvzf gcc-2.95.3.tar.gz
然后进入gcc-2.95.3目录给gcc打上补丁。
$cd gcc-2.95.3 $patch -p1< ../gcc-patch/gcc-2.95.3.-2.patch $patch -p1< ../gcc-patch/gcc-2.95.3.-no-fixinc.patch $patch -p1< ../gcc-patch/gcc-2.95.3-returntype-fix.patch echo timestamp > gcc/cstamp-h.in
在编译并安装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
如果没定义-Dinhibit,编译时将会报如下的错误。
../../gcc-2.95.3/gcc/libgcc2.c:41: stdlib.h: No such file or directory ../../gcc-2.95.3/gcc/libgcc2.c:42: unistd.h: No such file or directory make[3]: *** [libgcc2.a] Error 1 make[2]: *** [stmp-multilib-sub] Error 2 make[1]: *** [stmp-multilib] Error 1 make: *** [all-gcc] Error 2
如果没有定义-D__gthr_posix_h,编译时会报如下的错误。
In file included from gthr-default.h:1, from ../../gcc-2.95.3/gcc/gthr.h:98, from ../../gcc-2.95.3/gcc/libgcc2.c:3034: ../../gcc-2.95.3/gcc/gthr-posix.h:37: pthread.h: No such file or directory make[3]: *** [libgcc2.a] Error 1 make[2]: *** [stmp-multilib-sub] Error 2 make[1]: *** [stmp-multilib] Error 1 make: *** [all-gcc] Error 2
还有一种与-Dinhibit同等效果的方法,那就是在配置configure时多加一个参数-with-newlib,这个选项不会迫使我们必须使用newlib。编译bootstrap-gcc后,仍然可以选择任何c库。接着就是配置boostrap 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的支持。
接着编译并安装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的辅助测试工具,可以用它来分析和优化程序。
5.建立c库(glibc)
首先解压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 -xzvf glibc-linuxthreads-2.2.3.tar.gz --directory=glibc-2.2.3
然后进入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变量设成刚编译完的boostrap gcc,用它来编译glibc。
● --enable-add-ons告诉glibc用linuxthreads包,在上面已经将它放入了glibc源代码目录中,这个选项等价于Linuxthreads,即 -enable-add-ons=linuxthreads。
● --with-headers告诉glibc当前Linux内核头文件的目录位置。配置完后就可以编译和安装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)
这样,连接程序ld就会在libc.so所在的目录查找它需要的库,因为/lib目录可能已经装了一个相同名字的库,一个为编译可以在宿主机上运行的程序的库,而不是用于交叉编译的。
6.建立全套编译器(full gcc)
在建立boot-gcc的时候,只支持了C语言。到这里,这就要建立全套编译器,来支持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++语言。
然后编译和安装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++编译器。
完成以上操作后,基于Linux的ARM交叉编译环境建立即完成了。
3.2 工程管理器make
3.2.1 make概述
无论是在Linux还是在UNIX环境中,make都是一个非常重要的编译命令。无论是自己进行项目开发还是安装应用软件,都需要使用make或make install工具。利用make工具,可以将大型的开发项目分解成多个更易于管理的模块,对于一个包括几百个源文件的应用程序,使用make和makefile工具就可以清晰地理顺各个源文件之间的关系,而且如此多的源文件,如果每次都要键入gcc命令进行编译的话,这对程序员来说是无法忍受的。而make工具可自动完成编译工作,并且可以只对程序员在上次编译后修改过的部分进行编译。因此,有效地利用make和makefile工具可以大大提高项目开发的效率。
1.make是如何工作的
make工具最基本的功能就是通过makefile文件来描述源程序之间的相互依赖关系,并自动维护编译工作。当然,makefile文件需要按照某种语法进行编写,文件中需要说明如何编译各个源文件并连接生成可执行文件,要求定义源文件之间的依赖关系。makefile文件是许多编译器(包括Windows下的编译器)维护编译信息的常用方法,只是在集成开发环境中,用户可以通过友好的界面修改makefile文件而已。
例如,在当前目标下有一个文件名为“makefile”的文件,其内容如下(此文件的语法结构在接下来的内容中介绍)。
#It is a example for describing makefile edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o cc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o main.o : main.c defs.h cc -c main.c kbd.o : kbd.c defs.h command.h cc -c kbd.c command.o : command.c defs.h command.h cc -c command.c display.o : display.c defs.h buffer.h cc -c display.c insert.o : insert.c defs.h buffer.h cc -c insert.c search.o : search.c defs.h buffer.h cc -c search.c files.o : files.c defs.h buffer.h command.h cc -c files.c utils.o : utils.c defs.h cc -c utils.c clean : rm edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o
这个描述文档就是一个简单的makefile文件。在这个例子中:
第一个字符为#的行为注释行。
第一个非注释行指定edit由目标文件main.o kbd.o command.o display.o insert.o search.o files.o utils.o链接生成,这只是说明一个依赖关系。
第三行描述了如何从edit所依赖的文件建立可执行文件,即执行命令cc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o”,即调用gcc编译以上.o文件生成edit可执行文件。
接下来的各行分别指定各自的目标文件,以及它们所依赖的.c和.h文件。而紧跟的依赖关系行则指定了如何从目标所依赖的文件中建立目标。
在默认的方式下,在当前目录提示符下输入“make”命令。系统将自动完成以下操作。
(1)make会在当前目录下寻找名字为“Makefile”或“makefile”的文件。
(2)如果找到,它会查找文件中的第一个目标文件(target),在上面的例子中,系统将查找到“edit”这个目标,并把这个文件作为最终的目标文件。
(3)如果edit文件不存在,或是edit所依赖的后面的.o文件的文件修改时间要比edit这个文件新,那么,系统就会执行后面所定义的命令来生成edit这个文件。
(4)如果edit所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到,则根据这一个规则生成.o文件。
(5)如果此文件中列出的*.C文件和*.H文件都存在,于是make会首先生成.o文件,然后再用.o文件生成可执行文件edit。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在查找的过程中,如果出现错误,比如,最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性。
通常,makefile中还定义clean目标,可用来清除编译过程中的中间文件,例如上例中的内容:
clean : rm edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o
另外更为简单的方法为:
clean: rm -f *.o
在上述makefile文件中,像clean这个没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,可以在命令中要求执行,即使用命令“make clean”,以此来清除所有的目标文件,以便重编译。
在 Linux系统中,习惯使用 Makefile作为makefile文件。如果要使用其他文件作为makefile,则可利用以下make命令选项来指定makefile文件。
$ make -f filename
2.Makefile文件
Makefile文件中主要包含了5项内容:显式规则、隐晦规则、变量定义、文件指示和注释。
(1)显式规则。显式规则说明如何生成一个或多个目标文件。这是由Makefile的书写者明确指出要生成的文件、文件的依赖文件、生成的命令。
(2)隐晦规则。由于make有自动推导的功能,所以隐晦的规则可以让程序员比较粗糙、简略地书写Makefile,这是make所支持的。
(3)变量的定义。在Makefile中需要定义一系列的变量,变量一般都是字符串,这点类似于C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
(4)文件指示。包括3部分,一个是在一个Makefile文件中引用另一个Makefile文件,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;另外还可以定义一个多行的命令。
(5)注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用“#”字符,类似于C/C++中的“//”。如果需要在Makefile中使用“#”字符,可以用反斜框进行转义,如:“\#”。
从语法结构中来看,Makefile文件作为一种描述文档一般需要包含以下内容。
● 宏定义。
● 源文件之间的相互依赖关系。
● 可执行的命令。
Makefile中允许使用简单的宏指代源文件及其相关编译信息,在Linux中也称宏为变量。在引用宏时只需在变量前加$符号,但值得注意的是,如果变量名的长度超过一个字符,在引用时就必须加圆括号()。
下面都是有效的宏引用:
$(CFLAGS) $2 $Z $(Z)
最后两个引用是完全一致的。需要注意的是一些预定义宏,在Linux系统中,$*、$@、$?和$<4个特殊宏的值在执行命令的过程中会发生相应的变化,而在GNU make中定义了更多的预定义变量。例如:
# Define a macro for the object files OBJECTS= filea.o fileb.o filec.o # Define a macro for the library file LIBES= -LS # use macros rewrite makefile prog: $(OBJECTS) cc $(OBJECTS) $(LIBES) -o prog ……
此时如果执行不带参数的make命令,将连接3个目标文件和库文件LS;但是如果在make命令后带有新的宏定义:
make "LIBES= -LL -LS"
则命令行后面的宏定义将覆盖makefile文件中的宏定义。若LL也是库文件,此时make命令将连接3个目标文件及两个库文件LS和LL。
3.Make命令
Make命令可带有4 种参数:标志、宏定义、描述文件名和目标文件名。其标准形式为:
Make [flags] [macro definitions] [targets]
Linux系统下标志位flags选项及其含义如下。
-f file:指定file文件为描述文件,如果file参数为“-”符,那么描述文件指向标准输入。如果没有“-f”参数,则系统将默认当前目录下名为makefile或者名为Makefile的文件为描述文件。GNU make工具在当前工作目录中按照GNUmakefile、makefile、Makefile的顺序搜索makefile文件。
-i:忽略命令执行返回的出错信息。
-s:沉默模式,在执行之前不输出相应的命令行信息。
-r:禁止使用build-in规则。
-n:非执行模式,输出所有执行命令,但并不执行。
-t:更新目标文件。
-q:make操作将根据目标文件是否已经更新返回“0”或非“0”的状态信息。
-p:输出所有宏定义和目标文件描述。
-d:Debug模式,输出有关文件和检测时间的详细信息。
-c di:在读取makefile之前改变到指定的目录dir。
-I dir:包含其他makefile文件时,利用该选项指定搜索目录。
-h:help文档,显示所有的make选项。
-w:在处理makefile之前和之后,都显示工作目录。
通过命令行参数中的target,可指定make要编译的目标,并且允许同时定义编译多个目标,操作时按照从左向右的顺序依次编译target选项中指定的目标文件。如果命令行中没有指定目标,则系统默认target指向描述文件中第一个目标文件。
3.2.2 Makfile文件书写规则
在Makefile中规则的顺序是很重要的,因为Makefile中只应该有一个最终目标,其他的目标都是被这个目标所引入的,所以一定要让make知道最终目标是什么。一般来说,定义在Makefile中的目标可能会有很多,则第一条规则中的目标将被确立为最终的目标。如果第一条规则中的目标有很多个,那么,第一个目标会成为最终的目标。make所完成的也就是这个目标。
1.规则举例
foo.o : foo.c defs.h # foo模块 cc -c -g foo.c
在此例中,foo.o是目标,foo.c和defs.h是目标所依赖的源文件,而只有一个命令“cc-c -g foo.c”(此行一定要以Tab键开头)。这个规则有以下两个主要内容。
(1)文件的依赖关系,foo.o依赖于foo.c和defs.h的文件,如果foo.c和defs.h的文件日期比foo.o文件日期要新,或是foo.o不存在,那么依赖关系发生。
(2)第二行的cc命令说明了如何生成foo.o这个文件(当然foo.c文件include了defs.h文件)。
2.规则的语法
targets : prerequisites command ... 或是这样: targets : prerequisites ; command command ...
targets是文件名。一般来说,目标基本上是一个文件,但也有可能是多个文件,以空格分开,可以使用通配符。
command是命令行,如果不与“target:prerequisites”在一行,那么,必须以Tab键开头,如果和prerequisites在一行,那么可以用分号作为分隔。
prerequisites也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件比目标文件新(时间),那么,目标就被认为是“过时的”,被认为是需要重新生成的。如果命令太长,可以使用反斜框(‘\’)作为换行符。make对一行上有多少个字符没有限制。规则告诉make两件事,文件的依赖关系和如何生成目标文件。
3.在规则中使用通配符
如果想定义一系列比较类似的文件,很自然地就想起使用通配符。make支持3 种通配符:“*”,“?”和“[...]”。这与 B-Shell是相同的。另外,波浪号(“~”)字符在文件名中也有比较特殊的用途。如果是“~/test”,这就表示当前用户的$HOME目录下的test目录。而“~yzd/test”则表示用户yzd的宿主目录下的test目录。
通配符代替了一系列内容,如“*.c”表示所有以后缀为c的文件。需要注意的是,如果文件名中有通配符,如:“*”,则可以用转义字符“\”,如“\*”来表示真实的“*”字符。
通配符使用在规则中:
print: *.c lpr -p $? touch print
目标print依赖于所有的[.c]文件。
通配符同样用在变量中:
objects = *.o
在这里,objects的值就是“*.o”。Makefile中的变量其实就是C/C++中的宏。如果需要让通配符在变量中展开,也就是让objects的值是所有[.o]的文件名的集合,那么,可以这样定义:
objects := $(wildcard *.o)
4.文件搜寻
在一些大的工程中,有大量的源文件,由于通常的做法是将这许多的源文件分类存放在不同的目录中。所以,当make需要去找寻文件的依赖关系时,可以在文件前加上路径,但最好的方法是把路径告诉make,让make自动查找。
Makefile文件中的特殊变量“VPATH”就是完成这个功能的,如果没有指明这个变量, make只会在当前的目录中去查找依赖文件和目标文件。如果定义了这个变量,那么,make在当前目录找不到相关文件的情况下,会自动到所指定的目录中查找文件。
VPATH = src:../headers
这句代码指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。
另一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,是小写的),这不是变量,而是一个make的关键字,这与上面提到的那个VPATH变量很类似,但是它更为灵活,它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能,它的使用方法有3种:
(1)vpath <pattern> <directories>:为符合模式<pattern>的文件指定搜索目录<directories>。
(2)vpath <pattern>:清除符合模式<pattern>的文件的搜索目录。
(3)vpath:清除所有已被设置好了的文件搜索目录。
vapth使用方法中的<pattern>需要包含“%”字符。“%”的意思是匹配零个或若干字符,例如,“%.h”表示所有以“.h”结尾的文件。<pattern>指定了要搜索的文件集,而<directories>则指定了<pattern>的文件集的搜索的目录。例如:
vpath %.h ../headers
表示要求make在“../headers”目录下搜索所有以“.h”结尾的文件(如果某文件在当前目录没有找到的话)。
程序员可以连续地使用vpath语句,以指定不同搜索策略。如果连续的vpath语句中出现了相同的<pattern>,或是被重复了的<pattern>,那么,make会按照vpath语句的先后顺序来执行搜索。如:
vpath %.c foo vpath %.c blish vpath %.c bar
其表示“.c”结尾的文件,先在“foo”目录,然后是“blish”,最后是“bar”目录。
5.伪目标
在前面内容中提到过一个“clean”的目标,这是一个“伪目标”。
clean: rm *.o temp
此段语法并不是要生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。因此,只有通过指明这个“目标”才能让其生效(“伪目标”的取名不能和文件名重名)。为了避免和文件重名的这种情况,可以使用一个特殊的标记“.PHONY”来指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。即
.PHONY : clean
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make clean”命令有效。即整个这段代码为:
.PHONY: clean clean: rm *.o temp
一般情况下,伪目标没有依赖的文件。但是,也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。例如,Makefile文件需要生成若干个可执行文件,但用户又只想简单地敲一个make命令,并且,所有的目标文件都写在一个Makefile文件中,那么此时可以使用“伪目标”这个特性。
all : prog1 prog2 prog3
.PHONY : all prog1 : prog1.o utils.o cc -o prog1 prog1.o utils.o prog2 : prog2.o cc -o prog2 prog2.o prog3 : prog3.o sort.o utils.o cc -o prog3 prog3.o sort.o utils.o
Makefile中的第一个目标会被作为其默认目标。这里声明了一个“all”的伪目标,其依赖于其他3个目标。由于伪目标总是被执行的,所以其依赖的那3个目标就总是不如“all”这个目标新。所以,其他3个目标的规则总是会被执行。“.PHONY : all”声明了“all”这个目标为“伪目标”。
6.多目标
Makefile规则中的目标可以不止一个,即支持多目标,有可能多个目标同时依赖于一个文件,并且其生成的命令大体类似。因此可以将其合并起来。
例如:
bigoutput littleoutput : text.g generate text.g -$(subst output,,$@) > $@
上述规则等价于:
bigoutput : text.g generate text.g -big > bigoutput littleoutput : text.g generate text.g -little > littleoutput
其中,-$(subst output,,$@)中的“$”表示执行一个Makefile的函数,函数名为subst,后面的为参数。
关于Makefile文件的规则有很多,本书仅简要介绍基础内容,如果读者需要进一步深入研究,请参阅相关Makefile书籍。
3.3 Linux C/C++程序设计
C/C++适合编写基于嵌入式Linux的应用程序,Linux除操作系统接口外的函数库非常丰富,对16 位或更长的数据类型的算法的重复编码任务能自动生成代码。它还能对硬件进行特殊性的直观处理,一个串行Flash闪存设备的读或写能用C语言表达为一个简单的赋值语句,尽管存储操作需要一些编码。由于C/C++的资料较多,读者也比较了解,这里仅对Linux下的C/C++设计进行简单的概括。
3.3.1 C/C++程序结构
每个C/C++程序通常分为两个文件。一个文件用于保存程序的声明(declaration),称为头文件。另一个文件用于保存程序的实现(implementation),称为定义(definition)文件。
C/C++程序的头文件以“.h”为后缀,C程序的定义文件以“.c”为后缀,C++程序的定义文件通常以“.cpp”为后缀(也有一些系统以“.cc”或“.cxx”为后缀)。其中定义文件有3部分内容。
(1)定义文件开头处的版权和版本声明。
(2)对一些头文件的引用。
(3)程序的实现体(包括数据和代码)。
1.头文件
头文件由3部分内容组成。
(1)头文件开头处的版权和版本声明;头文件和定义文件的开头有时还有版权和版本的声明,如版权信息、当前版本号、作者/修改者、完成日期等,但一般不需要。
(2)预处理块(如文件包含命令include);
(3)函数和类结构声明等。
在C++语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。建议将成员函数的定义与声明分开,不论该函数体有多么小。
早期的编程语言如 Basic、Fortran没有头文件的概念,C/C++语言的头文件的作用主要有以下两点。
(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎样实现的。编译器会从库中提取相应的代码。
(2)能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
2.目录
如果一个软件的头文件数目比较多(如超过10个),通常应将头文件和定义文件分别保存于不同的目录,以便于维护。
例如,可将头文件保存于include目录,将定义文件保存于source目录(可以是多级目录)。如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。
3.程序
C/C++程序由标识符、关键字、运算符、分隔符、常量、注释符组成,它们又由字母、数字和空白符(空格符、制表符、换行符)构成。
(1)变量名、函数名、标号等统称为标识符。
标识符最好采用英文单词或其组合,便于记忆和阅读。标识符的长度应当符合“min-length && max-information”原则,而且命名规则尽量与所采用的操作系统或开发工具的风格保持一致。
例如,Windows应用程序的标识符通常采用“大小写”混排的方式,如AddChild。而UNIX、Linux应用程序的标识符通常采用“小写加下画线”的方式,如add_child。别把这两类风格混在一起用。
(2)C/C++的关键字分为以下几类。
● 类型说明符:用于定义、说明变量、函数或其他数据结构的类型,如int、double。
● 语句定义符:用于表示一个语句的功能。如if-else就是条件语句的语句定义符。
● 预处理命令字:用于表示一个预处理命令。如前面各例中用到的include。
(3)C/C++中含有相当丰富的运算符,运算符与变量、函数一起组成表达式,表示各种运算功能。运算符由一个或多个字符组成,其中单目运算符优先级较高,赋值运算符优先级低;算术运算符优先级较高,关系和逻辑运算符优先级较低。
表3-1给出了C/C++的运算符。
表3-1 C语言的运算
(4)分隔符有逗号和空格两种。逗号主要用在类型说明和函数参数表中,分隔各个变量。空格多用于语句各单词之间做间隔符。在关键字、标识符之间必须要有一个以上的空格符做间隔,否则将会出现语法错误,例如,把“int a;”写成“inta;”将出错。
(5)常量可分为数字常量、字符常量、字符串常量、符号常量、转义字符等。表3-2给出了C/C++的转义符和格式字符。
表3-2 C/C++的转义符和格式字符
(6)注释符是以“/*”开头并以“*/”结尾或者以“//”(C89以后的编译器支持,C++或 C#一贯都支持)开始的串。在“/*”和“*/”之间或“//”以后的即为注释。程序编译时,不对注释做任何处理。
3.3.2 C/C++数据类型
1.基本数据类型
C语言的数据类型可分为基本类型、构造类型、指针类型、空类型。其中,基本数据类型是自我说明的;构造类型包括数组类型/结构类型/联合类型,构造类型其成员还是基本类型或构造类型;指针类型用来表示某个量在内存储器中的地址;空类型其类型说明符为void,调用后并不需要向调用者返回函数值。限于篇幅,这里仅介绍基本类型。
(1)基本类型的分类及特点,如表3-3所示。
表3-3 基本类型的分类及特点
基本数据类型量,按其取值是否可改变又分为常量和变量两种。在程序中,常量是可以不经说明而直接引用的,而变量则必须先说明后使用。
C 语言用#define来定义常量(称为宏常量)。C++语言除了#define外还可以用const来定义常量(称为const常量)。其中,auto为自动变量;register为寄存器变量;extern为外部变量;static为静态变量。
(2)数据类型转换有以下两种。
● 自动转换。在不同类型数据的混合运算中,由系统自动实现转换,由少字节类型向多字节类型转换。不同类型的量相互赋值时也由系统自动进行转换,把赋值号右边的类型转换为左边的类型。
● 强制转换。由强制转换运算符完成转换。
2.数组
如果一个变量名后面跟着一个有数字的中括号,这个声明就是数组声明。字符串也是一种数组。它们以ASCII的NULL作为数组的结束。如:
float mymatrix [3] [2] = {2.0 , 10.0, 20.0, 123.0, 1.0, 1.0} char lexicon [10000] [300] ; /*共10 000个最大长度为300的字符数组。*/ int a[3][4];
上面最后一个例子创建了一个数组,但也可以把它看成是一个多维数组。注意数组的下标从0开始。这个数组的结构如下:
a[0][0] a[0][1] a[0][2] a[0][3] a[1][0] a[1][1] a[1][2] a[1][3] a[2][0] a[2][1] a[2][2] a[2][3]
3.指针
如果一个变量声明时在前面使用*号,表明这个变量是一个指针。定义指针的目的是为了通过指针去访问内存单元。例如:
int *pi; /* 指向整型数据的指针 */ int *api[3]; /* 指向整型数据的一个三维数组指针 */ char **argv; /* 指向一个字符指针的指针 */
储存在指针中的地址所指向的数值在程序中可以由*读取,如*pi是一个整型数据,引用了一个指针。
另一个运算符&,是取地址运算符,它将返回一个变量、数组或函数的存储地址。如下面的例子:
int i, *pi; /* int and pointer to int */ pi = &i;
i和*p在程序中可以相互交替使用,直到pi被改变成指向另一个变量的指针。
3.3.3 表达式/语句、函数
1.表达式与语句
C/C++表达式是由运算符连接常量、变量、函数所组成的式子,每个表达式都有一个值和类型。表达式求值按运算符的优先级和结合性所规定的顺序进行。表达式和语句都属于C/C++的短语结构语法。
(1)复合语句。
如a = b = c = 0这样的表达式称为复合表达式。C语言中的复合语句的格式为:
{语句;语句;……}
复合语句可以使得几个语句变成一个语句。允许复合表达式存在的理由是:书写简洁,可以提高编译效率。但要防止滥用复合表达式。
(2)条件语句。
C/C++主要有3种条件语句形式。两种是if语句,另一种是switch语句。
在if条件表达式中,任何非零的值表示条件为真;如果条件不满足,程序将跳过if后面的语句,直接执行if后面的语句。但是如果if后面有else,则当条件不成立时,程序跳到else处执行。两种if语句包括:
/****1****/ if(条件表达式) 语句; /****2****/ if(条件表达式) 语句; else 语句;
switch是多分支选择语句,而if语句只有两个分支可供选择。虽然可以用嵌套的if语句来实现多分支选择,但那样的程序冗长难读。
switch通常用于对几种有明确值的条件进行控制,它要求的条件值通常是整数或字符。与switch搭配的条件转移是case。使用case后面的标值,控制程序将跳到满足条件的case处一直往下执行,直到语句结束或遇到break。通常可以使用default把其他例外的情况包含进去。如果switch语句中的条件不成立,控制程序将跳到default处执行。switch是可以嵌套的。
switch (<表达式>) { case <值1> : <语句> case <值2> : <语句> default : <语句> }
(3)循环语句。
C/C++有三种形式的循环语句:
/****1****/ do <语句> while (<表达式>); /****2****/ while (<表达式>) <语句>; /****3****/ for (<表达式1> ; <表达式2> ; <表达式3>) <语句>;
在while和do中,语句将执行到表达式的值为零时结束。在do…while语句中,循环体将至少被执行一次。这3种循环结构可以互相转化。
for (e1; e2; e3) s;
相当于:
e1; while (e2) { s; e3; }
当循环条件一直为真时,将产生死循环。
C/C++循环语句中,for语句使用频率最高,while语句其次,do语句很少用。提高循环体效率的基本办法是降低循环体的复杂性。
在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨越循环层的次数。
(4)跳转语句。
跳转语句主要有continue,break和return。
● continue语句用在循环语句中,作用是结束当前一轮循环,马上开始下一轮循环。
● break语句用在循环语句或switch中,作用是结束当前循环,跳到循环体外继续执行。但是使用break只能跳出一层循环。在要跳出多重循环时,可以使用goto使得程序更为简洁。
● 当一个函数执行结束后要返回一个值时,使用return。return可以跟一个表达式或变量。如果return后面没有值,将执行不返回值。
2.函数
C/C++函数由返回值、函数名、参数列表(或void表示没有返回值)和函数体组成,函数体的语法和其他的复合语句部分是一样的。C/C++各类函数不仅数量多,而且有的还需要硬件知识才会使用,因此要想全部掌握则需要一个较长的学习过程。这里仅做粗略的讲述。
函数接口的两个要素是参数和返回值。在C语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。C++语言中多了引用传递(pass by reference)。由于引用传递的性质像指针传递,而使用方式却像值传递,初学者常常迷惑不解,容易引起混乱。
在C/C++中,程序从main开始执行。main函数通过调用和控制其他函数进行工作。程序员可以自己写函数,或从库中调用函数。如语句return 0;可使得main返回一个值给调用程序的外壳,表明程序已经成功运行。
(1)输入输出函数。
C语言中没有提供专门的输入输出语句,所有的输入输出都是由调用标准库函数中的输入输出函数来实现的,其函数原型在头文件“stdio.h”中。
scanf和getchar函数是输入函数,接收来自键盘的输入数据。scanf是格式输入函数,可按指定的格式输入任意类型的数据;getchar函数是字符输入函数,只能接收单个字符。
printf和putchar函数是输出函数,向显示器屏幕输出数据。printf是格式输出函数,可按指定的格式显示任意类型的数据;putchar是字符显示函数,只能显示单个字符。
(2)字符串函数。
C/C++提供了丰富的字符串处理函数,大致可分为字符串的输入、输出、合并、修改、比较、转换、复制、搜索几类。使用这些函数可大大减轻编程的负担。用于输入输出的字符串函数,在使用前应包含头文件“studio.h”,使用其他字符串函数则应包含头文件“string.h”。
(3)内存管理函数。
在实际的编程中,所需的内存空间往往取决于实际输入的数据,而无法预先确定。对于这种问题,用数组的办法很难解决。为了解决上述问题,C/C++提供了一些内存管理函数,这些内存管理函数可以按需要动态地分配内存空间,也可把不再使用的空间回收待用,为有效地利用内存资源提供了手段。
● 分配内存空间函数malloc(size)表示在内存的动态存储区中分配一块长度为“size”字节的连续区域,函数的返回值为该区域的首地址。
● 分配内存空间函数calloc(n,size)也用于分配内存空间。calloc函数与malloc函数的区别仅在于一次可以分配n块区域。
● 释放内存空间函数free(void*ptr);表示释放ptr所指向的一块内存空间。
(4)用户自定义函数。
C/C++函数大部分是用户编写的函数,这种函数不仅要在程序中定义函数本身,而且在主调函数模块中还必须对该被调函数进行类型说明,然后才能使用。
编写自定义函数时需要注意:
● 避免函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。如果函数没有参数,则用void填充。
● 不要省略返回值的类型。如果函数没有返回值,那么应声明为void类型。
● 函数的功能要单一,不要设计多用途的函数。函数体的规模要小,尽量控制在50行代码之内。
● 尽量避免函数带有“记忆”功能,相同的输入应当产生相同的输出。带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的“记忆”存储器。建议尽量少用static局部变量,除非必须使用。
● 不仅要检查输入参数的有效性,还要检查通过其他途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。
3.3.4 C/C++设计注意事项
1.C/C++内存分配方式
一个由C/C++编译的程序占用的内存分为以下几个部分。
(1)栈区(stack):由编译器(Compiler)自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
(2)堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
(3)全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
(4)文字常量区:常量字符串就是放在这里。程序结束后由系统释放。
(5)程序代码区:存放函数体的二进制代码。
(1)内存分配方式
内存分配方式有下面三种。
● 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量、static变量。
● 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
● 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由用户决定,使用非常灵活。
(2)内存问题总结
发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。首先需要区分栈与堆的不同。
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确地释放本内存空间。另外,由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动地将多余的那部分重新放入空闲链表中。
堆和栈的区别可以用如下的比喻来看出,使用栈就像去饭馆里吃饭,去商场买衣服,好处是快捷,不用考虑太多,但是自由度小,有大小限制。使用堆就像是自己动手,丰衣足食,比较麻烦,但是比较符合自己,而且自由度大。
常见的内存错误及其对策如下。
● 内存分配未成功,却使用了它。刚编程时容易犯这种错误,因为没有意识到内存分配会不成功。常用解决办法是在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
● 内存分配虽然成功,但是尚未初始化就引用它。犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的默认初值全为零,导致引用初值错误(例如数组)。
● 内存的默认初值究竟是什么并没有统一的标准,尽管有些时候为零值。无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
● 内存分配成功并且已经初始化,但操作越过了内存的边界。例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
● 忘记了释放内存,造成内存泄漏。含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,用户看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
如果释放了内存却继续使用它,将出现下面三种情况。
程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
使用free或delete释放了内存后,没有将指针设置为NULL,导致产生“野指针”。
2.C/C++编程常见问题
下面介绍C/C++编程中常见的一些问题及其处理办法。
(1)内存分配
用malloc或farmalloc动态分配内存时,如
char *buffer; buffer=(char *)malloc(300);
因为并不是在所有的情况下,都会分配成功,所以应加if(buffer==NULL) {......}char的取值范围为0~127。
unsigned char的取值范围为0~255。
在256色的屏幕模式下,颜色索引值为0~255。
(2)指针问题
● 指针与指针数组
int (*p)[4]; //定义一个指向包含4个整数元素的指针 int *p[4]; //定义一个指针数组,该指针数组包含4个指向整形变量的指针。
char **argv等价于char *argv[]。
Linux程序中经常出现int main(int argc, char * argv[]),其中argc是外部命令参数的个数,argv[]存放的是各参数的内容。
例如下面一段程序。
include <iostream> #include <cstring> using namespace std; int main(int argc, char* argv[]) { char *str1 = argv[1]; char *str2 = argv[2]; int result; if ((result = strcmp(str1,str2)) == 0) cout << "equal" << endl; else { if (result > 0) cout << str1 << ">" << str2 << endl; else cout << str1 << "<" << str2 <<endl; } return 0; }
经过编译后,在命令行输入./test abc def ,输出结果为abc<edf。
abc就是argv[1], def为argv;而argv[0]是文件名test.cpp。由于有两个参数,故argc=2。
● 函数指针与指针函数
可以这么理解:函数指针是指针,是指向函数的指针;指针函数是函数,是返回一个指针的函数。
int (*p)(); //函数指针,也就是函数的入口地址。 int *p(); //指针函数,也就是函数返回的值是一个指针。
例如:
(1)int (*p)(char);
这里p被声明为一个函数指针,这个函数带一个char类型的参数,并且有一个int类型的返回值。
(2)常见内存拷贝
unsigned char *memcpy(unsigned char *dest,const unsigned char *src, int lenth);
这是一个函数返回值为unsigned char类型的指针。
3.Linux C链接库开发
在Linux下有静态库(static library,.a文件)和共享库(shared library,.so文件)之分,就像Win32系统中,存在静态库(.lib)和动态库(.dll)一样。静态库和共享库都是一个obj文件的集合,但静态链接后,执行程序中存在自己所需obj的一份拷贝;而动态链接后,执行程序仅仅是包含对共享库的一个引用。共享库相当于一个由多个obj文件组合而成的obj文件,在链接后其所有代码被加载,不管需要的还是不需要的。
静态库与动态库具体区别如下:
● 静态链接后的程序比动态链接的所用存储空间大,因为执行程序中包含了库中代码拷贝;而动态链接的程序比静态链接的所用的运行空间大,因为它将不需要的代码也加载到运行空间。
● 动态链接库虽然可以类似插件给编译好了的二进制程序实现一些功能及升级,但程序发布的时候,需要随身携带那些.so文件,而静态库是一劳永逸,编译后不需要带一堆库文件,而且不管放置到哪里都可正常运行。
(1)静态库
● 字符串转换
建立一个简单的静态库,实现一字符串大小写相互转换,需要建立下面三个文件。
头文件convert.h
它声明相关函数原形,内容如下:
#ifndef CONVERT_H #define CONVERT_H extern char* toLowerString(char* sSource); extern char* toUpperString(char* sSource); #endif
toLower.c
函数把字符串转换成小写字符串,内容如下:
char* toLowerString(char* src) { assert(src != NULL); char* des = src; while (*des) { if (isupper(*des)) { *des = tolower(*des); } des++; } return src; }
toUpper.c
函数把字符串转换成大写的写字符串,内容如下:
char* toUpperString(char* src) { assert(src != NULL); char* des = src; while (*des) { if (islower(*des)) { *des = toupper(*des); } des++; } return src; }
● 生成静态库
利用GCC生成对应目标文件。
gcc -c toLower.c toUpper.c
gcc首先会对文件进行编译,生成toLower.o和toUpper.o两个目标文件(相当于Windows下的obj文件)。
然后用ar创建一个名字为libconvert.a的库文件,并把toLower.o和toUpper.o的内容插入到对应的库文件中。相关命令如下。
ar -rcs libconvert.a toLower.o和toUpper.o
命令执行成功以后,对应的静态库libconvert.a已经成功生成。
● 测试
#include <stdio.h> #include <ctype.h> #include <assert.h> #include <convert.h> int main() { char a[] = "abcd"; char b[] = "ABCD"; printf("%s\n", toUpperString(a, 0)); printf("%s\n", toLowerString(b, 1)); return 0; }
静态库的调用比较简单,跟标准库函数调用一样,如果把头文件和库文件复制到gcc默认的头文件目录/usr/include和/usr/lib里面,编译程序就跟标准库函数调用一模一样了。
gcc -o test main.c -I头文件的路径 -L库文件的路进 -lconvert gcc main.c -o test -static -L. -lconvert
(2)动态链接库
Linux动态链接库为隐式调用和显式调用两种调用方法,下面分别介绍。
● 隐式调用
隐式调用的使用方法和静态库的调用差不多,在这种调用方式中,需要维护动态链接库的配置文件/etc/ld.so.conf来让动态链接库为系统所使用,通常将动态链接库所在目录名追加到动态链接库配置文件中。否则在执行相关的可执行文件的时候就会出现载入动态链接库失败的现象。在编译所引用的动态库时,可以在gcc采用 -l或-L选项或直接引用所需的动态链接库方式进行编译。在 Linux里面,可以采用ldd命令来检查程序依赖共享库。
按照正常编程,include方式只是在编译程序的时候加上 -Lxxx -lxxx的方式来利用动态链接库,这种方式利用了动态链接库升级方便的特点。在这种方式下,用ldd程序名能够查看到程序所使用的动态链接库。
下面生成动态链接库:
gcc -fPIC -shared -o libconvert.so toLower.o toUpper.o
-fPIC 使输出的对象模块是按照可重定位地址方式生成的。
-shared指定把对应的源文件生成对应的动态链接库文件libconvert.so文件。
对应的链接库已经生成,下面看一下如何使用对应的链接库。
gcc -g main.c -o test -L. -lconvert
为了执行程序,告诉动态链接器ld.so如何找到这个库。
LD_LIBRARY_PATH=$(PWD)
● 显式调用
显式调用是通过调用dlfcn系列函数所制作的程序,如dlopen函数。在这种方式下,程序可自由指定所要加载的动态链接库,适用于加载 Plug-in式的动态链接库。在这种方式下,用ldd程序名查看不到程序所使用的动态链接库。
#include<stdio.h> #include<dlfcn.h> int main(int argc, char* argv[]) { char a[] = "abcd"; char b[] = "ABCD"; //define function pointor int (*pUpper)(char* pStr); //声明对应函数的函数指针 int (*pLower)(char* pStr, ); void *pdlHandle; char *perr; pdlHandle = dlopen("./libconvert.so", RTLD_LAZY); //加载链接库 /libconvert.so if(!pdlHandle) { printf("Failed load library\n"); } perr = dlerror(); if(perr != NULL) {
printf("%s\n", perr); return 0; } //get function from lib pUpper = dlsym(pdlHandle, "toUpperString"); //获取函数的地址 perr = dlerror(); if(perr != NULL) { printf("%s\n", perr); return 0; } pLower = dlsym(pdlHandle, "toLowerString"); perr = dlerror(); if(perr != NULL) { printf("%s\n", perr); return 0; } printf("%s\n", pUpper(a); printf("%s\n", pLower(b); dlclose(pdlHandle); return 0; } gcc -o test -ldl main.c
用gcc编译对应的源文件生成可执行文件,-ldl选项表示生成的对象模块需要使用共享库。执行对应的文件同样可以得到正确的结果。
相关函数的说明如下:
● dlopen()
第一个参数,指定共享库的名称,将会在下面位置查找指定的共享库。
● dlsym()
调用dlsym时,利用dlopen()返回的共享库的phandle以及函数名称作为参数,返回要加载函数的入口地址。
● dlerror()
该函数用于检查调用共享库的相关函数出现的错误。
3.4 Linux汇编程序设计
汇编语言的优点是速度快,可以直接对硬件进行操作,虽然Linux是一个用C语言开发的操作系统,以下介绍Linux汇编语言的语法格式和开发工具。
作为最基本的编程语言之一,汇编语言虽然应用的范围不算很广,但重要性却毋庸置疑,因为它能够完成许多其他语言所无法完成的功能。在Linux操作系统中,虽然绝大部分代码是用C语言编写的,但仍然不可避免地在某些关键地方使用了汇编代码。
在大多数情况下,Linux程序员不需要使用汇编语言,因为即便是硬件驱动这样的底层程序,在Linux操作系统中也可以完全用C语言来实现,加之GCC目前已经能够对最终生成的代码进行很好的优化。但是,在移植Linux到某一特定的嵌入式硬件环境下时,则需要汇编程序。汇编语言直接同计算机的底层进行交互,它具有以下一些优点。
● 能够直接访问与硬件相关的存储器或I/O端口;
● 能够不受编译器的限制,对生成的二进制代码进行完全的控制;
● 能够对关键代码进行更准确的控制,避免因线程共同访问或者硬件设备共享引起的死锁;
● 能够根据特定的应用对代码做最佳的优化,提高运行速度;
● 能够最大限度地发挥硬件的功能。
当然,由于汇编语言作为一种层次非常低的语言,仅仅高于直接手工编写二进制的机器指令码,因此不可避免地存在一些缺点。
● 编写的代码非常难懂,不好维护;
● 很容易产生bug,难以调试;
● 只能针对特定的体系结构和处理器进行优化;
● 开发效率很低,时间长且单调。
Linux下用汇编语言编写的代码具有两种不同的形式。
第一种是完全的汇编代码,即整个程序全部用汇编语言编写。尽管是完全的汇编代码, Linux平台下的汇编工具也吸收了C语言的长处,使得程序员可以使用#include、#ifdef等预处理指令,并能够通过宏定义来简化代码。
第二种是内嵌的汇编代码,即可以嵌入到C语言程序中的汇编代码片段。虽然ANSI的C标准中没有关于内嵌汇编代码的相应规定,但各种实际使用的C编译器都做了这方面的扩充。
3.4.1 Linux汇编语法格式
在DOS/Windows下的汇编语言基本上都是Intel风格的。但在UNIX和Linux系统中,更多的是采用AT&T格式,两者在语法和格式上有着很大的不同。
在AT&T汇编格式中,寄存器名要加上“%”作为前缀;而在Intel汇编格式中,寄存器名不需要加前缀。例如:
AT&T 格式 :pushl %eax Intel 格式:push eax
在AT&T汇编格式中,用'$'前缀表示一个立即操作数;而在Intel汇编格式中,立即数的表示不用带任何前缀。例如:
AT&T 格式:pushl $1 Intel 格式:push 1
AT&T和Intel格式中的源操作数和目标操作数的位置正好相反。在Intel汇编格式中,目标操作数在源操作数的左边;而在AT&T汇编格式中,目标操作数在源操作数的右边。例如:
AT&T 格式:addl $1, %eax Intel 格式:add eax, 1
在AT&T汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀'b'、'w'、'l'分别表示操作数为字节(byte,8比特)、字(word,16比特)和长字(long,32比特);而在Intel汇编格式中,操作数的字长是用'byte ptr'和'word ptr'等前缀来表示的。例如:
AT&T 格式:movb val, %al Intel 格式:mov al, byte ptr val
在AT&T汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上'*'作为前缀,而在 Intel格式中则不需要。远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为'ljump'和'lcall',而在Intel汇编格式中则为'jmp far'和'call far',即:
AT&T 格式: ljump $section, $offset , lcall $section, $offset Intel 格式: jmp far section:offset , call far section:offset
与之相应的远程返回指令则为:
AT&T 格式:lret $stack_adjust Intel 格式:ret far stack_adjust
在AT&T汇编格式中,内存操作数的寻址方式为:
section:disp(base, index, scale)
而在Intel汇编格式中,内存操作数的寻址方式为:
section:[base + index*scale + disp]
由于 Linux工作在保护模式下,用的是32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地址计算方法:
disp + base + index * scale
下面是一些内存操作数的例子。
AT&T格式:
movl -4(%ebp), %eax movl array(, %eax, 4), %eax movw array(%ebx, %eax, 4), %cx movb $4, %fs:(%eax)
Intel格式:
mov eax, [ebp - 4] mov eax, [eax*4 + array] mov cx, [ebx + 4*eax + array] mov fs:eax, 4
3.4.2 汇编程序实例
以下介绍如何在 Linux系统下编写第一个汇编程序。Linux是一个运行在保护模式下的32位操作系统,采用flat memory模式,目前最常用到的是ELF格式的二进制代码。一个ELF格式的可执行程序通常划分为如下几个部分:.text、.data和.bss。
● text是只读的代码区。
● data是可读可写的数据区。
● bss则是可读可写且没有初始化的数据区。
代码区和数据区在ELF中统称为section,根据实际需要可以使用其他标准的section,也可以添加自定义的section,一个ELF可执行程序至少应该有一个.text部分。下面是一个AT&T格式的汇编语言程序。
# hello.s (AT&T格式) .data # 数据段声明 msg : .string "Hello, world!just test!\n" # 要输出的字符串 len = . - msg # 字串长度 .text # 代码段声明 .global _start # 指定入口函数 _start: # 在屏幕上显示一个字符串 movl $len, %edx # 参数三:字符串长度 movl $msg, %ecx # 参数二:要显示的字符串 movl $1, %ebx # 参数一:文件描述符(stdout) movl $4, %eax # 系统调用号(sys_write) int $0x80 # 调用内核功能 # 退出程序 movl $0,%ebx # 参数一:退出代码 movl $1,%eax # 系统调用号(sys_exit) int $0x80 # 调用内核功能
这是一个最简单的Linux汇编程序,其调用Linux内核提供的sys_write来显示一个字符串,然后再调用sys_exit退出程序。在Linux内核源文件include/asm-i386/unistd.h中,可以找到所有系统调用的定义。此段码如果采用Intel格式则为:
; hello.asm (Intel格式) section .data ; 数据段声明 msg db "Hello, world!", 0xA ; 要输出的字符串 len equ $ - msg ; 字串长度 section .text ; 代码段声明 global _start ; 指定入口函数 _start: ; 在屏幕上显示一个字符串 mov edx, len ; 参数三:字符串长度 mov ecx, msg ; 参数二:要显示的字符串 mov ebx, 1 ; 参数一:文件描述符(stdout) mov eax, 4 ; 系统调用号(sys_write) int 0x80 ; 调用内核功能 ; 退出程序 mov ebx, 0 ; 参数一:退出代码 mov eax, 1 ; 系统调用号(sys_exit) int 0x80 ; 调用内核功能
3.5 Linux Shell语言编程
在使用Linux操作系统时,每个用户登录系统后,系统总会出现不同的命令提示符,如#、$或者~等,然后用户输入的任何命令(正确的命令),系统都可以根据命令的要求执行,直到用户注销,在这期间,用户的所有命令都会经过解释才能被执行,而完成这一功能的机制就是Shell,如图3-1所示是Shell在Linux操作系统的位置。Shell具有以下主要特点。
图3-1 Shell在Linux操作系统中的位置
(1)对已有命令进行适当组合,构成新的命令,而组合方式很简单。
(2)它们提供了文件名扩展字符,使得用单一的字符串可以匹配多个文件名,省去键入一长串文件名的麻烦。
(3)可以直接使用Shell的内置命令,而不需要创建新的进程。
(4)Shell允许灵活地使用数据流,提供了通配符、输入/输出重定向、管道线等机制,方便了模式匹配、I/O处理和数据传输。
(5)结构化的程序模块,提供了顺序流程控制、条件控制、循环控制等。
(6)Shell提供了在后台执行命令的能力。
(7)Shell提供了可配置的环境,允许用户创建和修改命令、命令提示符和其他的系统行为。
(8)Shell提供了一个高级的命令语言,允许用户能创建从简单到复杂的程序。
在Linux和UNIX系统里可以使用多种不同的Shell。最常用的几种是Bourne Shell(sh), C Shell(csh) 和Korn Shell(ksh)。这3种Shell都有它们的优点和缺点。
(1)Bourne SHELL的作者是Steven Bourne,它是UNIX最初使用的Shell并且在每种UNIX上都可以使用。Bourne Shell在Shell编程方面相当优秀,但在处理与用户的交互方面不如其他几种Shell。
(2)C Shell由Bill Joy所写,它更多地考虑了用户界面的友好性,它支持如命令补齐(command-line completion)等一些Bourne Shell所不支持的特性。普遍认为C Shell的编程接口不如Bourne Shell,但C Shell被很多C程序员使用是因为C Shell的语法和C语言的语法很相似,这也是C Shell名称的由来。
(3)Korn Shell(ksh)由Dave Korn所写。它集合了C Shell和Bourne Shell的优点,并且和Bourne Shell完全兼容。
除了这些Shell以外,许多其他的Shell程序吸收了这些原来的Shell程序的优点而成为新的Shell。在Linux上常见的有tcsh(csh的扩展),Bourne Again Shell(bash、sh的扩展)和Public Domain Korn Shell(pdksh、ksh的扩展)。bash是大多数Linux系统的默认Shell。Bourne Again Shell(bash),正如它的名字所暗示的,是Bourne Shell的扩展。bash与Bourne Shell完全向后兼容,并且在Bourne Shell的基础上增加和增强了很多特性。bash也包含了很多C和Korn Shell里的优点。bash有很灵活和强大的编程接口,同时又有很友好的用户界面。
3.5.1 Shell环境变量及配置文件
因为Linux支持多种Shell,故读者可以根据个人习惯选择不同的Shell,要查看目录所使用的Shell或者系统默认Shell,只需要运行“echo”命令来查询Shell环境变量即可,用法如下:
[root@yangzongde root]# echo $SHELL /bin/bash //目前使用的是Bash [root@yangzongde root]# echo ${SHELL} /bin/bash 如果要改变当前使用的Shell,只需要运行该Shell程序名即可进行切换。 [root@yangzongde root]# sh sh-2.05b# bash [root@yangzongde root]# ash # bsh # tcsh [root@yangzongde ~]#
Shell环境变量(Environment Variables)是Shell用来保存系统信息的变量,这些变量可供Shell中运行的程序使用。不同的Shell有不同的环境变量以及环境变量值,运行“set”命令可以显示当前环境变量名及变量值。
[root@yangzongde ~]# set COLORS /etc/DIR_COLORS _ addsuffix argv () autologout 60 cwd /root dirstack /root dspmbyte euc echo_style both edit file /root/.i18n gid 0 group root history 100 home /root killring 30 owd path (/usr/local/sbin /usr/sbin /sbin /usr/local/sbin /usr/local/bin /sbin /bin /usr/sbin /usr/bin /usr/X11R6/bin /root/bin /usr/local/sbin /usr/ local/bin /root/bin) prompt [%n@%m %c]# prompt2 %R? prompt3 CORRECT>%R (y|n|e|a)? shell /bin/tcsh shlvl 4 sourced 1 status 0 tcsh 6.12.00 term vt100 tty pts/0 uid 0 user root version tcsh 6.12.00 (Astron) 2002-07-23 (i386-intel-linux) options 8b,nls,
dl,al,kan,rh,color,dspm,filec
在Linux操作系统中,有以下几个主要的与Shell有关的配置文件。
(1)/etc/profile文件:这是系统最重要的Shell配置文件,也是用户登录系统最先检查的文件,系统的环境变量多定义在此文件中。主要包括“PATH、USER、LGNAME、MAIL、HOSTNAME、HISTSIZE以及INPUTRC”。
(2)~/.bash_profile文件:每个用户的BASH环境配置文件,存在于用户的主目录中,当系统运行/etc/profile后,将读取此文件的内容,此文件定义了“USERNAME\BASH ENV和 PATH”等环境变量,此处的 PATH 包括了用户自己定义的路径,以及用户的“bin”路径。
(3)~/.bashrc文件:前两个文件仅在系统登录时读取,此文件将在每次运行bash时读取,此文件主要定义的是一些终端设置,以及 Shell提示符等功能,而不定义环境变量等内容。
(4)~/.bash_login文件:如果~/.bash_profile文件不存在,系统会读取这个文件的内容。
(5)~/.profile文件:如果~/.bash_profile和~/.bash_login文件都不存在时将读取此文件内容。
(6)~/.bash_history文件:记录了用户使用的历史命令。
3.5.2 Shell编程实例
Shell程序是一个包含UNIX命令的普通文件,这个文件的许可权限至少应该为可读和可执行。在Shell提示符下键入文件名就可执行Shell程序。Shell程序可以通过环境变量、命令行参数、用户的输入3种方式接收数据。
Shell是一个命令解释器,它会解释并执行命令提示符下输入的命令。如果用户想要多次执行一组命令,Shell提供了一种功能将这组命令存放在一个文件中,然后可以像Linux系统提供的其他程序一样执行这个文件,这个命令文件就叫做Shell程序或者Shell脚本程序。
当运行这个文件,它会如同在命令行输入这些命令一样执行这些命令,为了让 Shell能读取并且执行Shell程序,Shell脚本的文件权限必须被设置为可读和可执行。程序员可以写出非常复杂的Shell脚本,因为Shell脚本支持变量、命令行参数、交互式输入、tests (判断))、branches(分支)和loops(循环)等复杂的结构。以下是一个简单的Shell程序myshell运行过程(此程序源代码见光盘ch01文件夹)。
[root@yangzongde ~]# cat myshell #!/bin/bsh #这句不是注释,标识要使用的SHELL #this is a example of shell #注释 echo $SHELL #显示当前SHELL类型 echo "Hello World" #显示Hello World ls -l #列出当前上当文件详情 [root@yangzongde ~]# chmod u+x myshell #修改文件权限为可执行 [root@yangzongde ~]# ls -l myshell #查看文件权限 -rwxr--r-- 1 root root 76 4月 1 17:14 myshell [root@yangzongde ~]# ./myshell #运行此SHELL文件 /bin/bash #echo $SHELL运行结果 Hello World #echo "Hello World"运行结果
总用量36 # ls -l运行结果 -rw-r--r-- 1 root root 1276 3月22 05:44 anaconda-ks.cfg -rw-r--r-- 1 root root 20771 3月22 05:44 install.log -rw-r--r-- 1 root root 3956 3月22 05:44 install.log.syslog -rwxr--r-- 1 root root 76 4月1 17:14 myshell
一个Shell脚本程序编写的步骤如下。
(1)用编辑器(如VI)编辑包含所有操作的.sh文件;
(2)修改文件的权限为可读可执行;
(3)运行当前Shell程序。
Shell编程能够使系统管理员方便地执行系统相关操作,Shell编程是Linux环境下一个非常重要的编程内容。关于详细的Shell编程请读者参阅梁普选等编著的《Linux编程命令详解》(电子工业出版社出版)一书。
3.6 Linux Perl语言编程
Perl(Practical Extractionand Report Language)是一种解释性的语言,专门为搜索纯文本文件而做了优化。它也可以十分方便地完成很多系统管理任务。它集成了C、sed、awk和sh语言的优点,可以运行于Linux、UNIX、MVS、VMS、MS-DOS、Macintosh、OS/2、Amiga以及其他的一些操作系统上。特别是近年来,随着 Internet的普及,Perl也越来越多地用于WorldWideWeb上CGI等的编程,逐渐成为系统、数据库和用户之间的桥梁。
有两种程序员喜欢用Perl,系统程序员可以用Perl结合系统命令一起处理数据和过程,并且可以使用Perl的格式匹配函数进行系统信息的搜寻和总结;还有一些开发UNIX Web服务器CGI程序的程序员发现Perl比C语言易学易用,而且更容易处理数据库和数据搜索。
3.6.1 Perl基本程序
Perl的创建人Larry Wall在1994年10月发表了Perl的第5版本,目前发展到V5.8。Perl 5增加了面向对象的能力,提供了更多的数据结构、系统和数据库之间的新的标准接口,以及其他的一些功能。
例如,希望更换大量文件中的一些相同内容,可以使用下面的一条命令。
perl -e 's/gopher/World Wide Web/gi' -p -i.bak *.html
以下是一个基本的perl程序。
[root@yangzongde perl]# cat perl 显示程序内容 #!/usr/bin/perl # #Progarm to do the Example # print'hello World!'; #printf a message [root@yangzongde perl]# chmod u+x perl 加上执行权限 [root@yangzongde perl]# ls perl [root@yangzongde perl]# ./perl 运行程序 hello World![root@yangzongde perl]# 运行结果
每个perl程序都以# !/usr/bin/perl开始,这样系统的外壳知道应该使用perl运行该程序。Perl表达式必须以分号结尾,就如同C语言一样。此语句为显示语句,只是简单地显示出hello World字符串。
3.6.2 Perl变量
Perl中有3种变量:标量,数组(列表)和相关数组。
(1)标量。
Perl中最基本的变量类型是标量。标量既可以是数字,也可以是字符串,而且两者是可以互换的。具体是数字还是字符串,可以由上下文决定。标量变量的语法为$variable_name。例如:
$priority = 9;
把9赋予标量变量$priority,也可以将字符串赋予该变量。
$priority = 'high';
注意在Perl中,由于变量名的大小写是敏感的,所以$a和$A是不同的变量。
Perl中有3种类型的引用。
双引号("")括起来的字符串中的任何标量和特殊意义的字符都将被Perl解释。如果不想让Perl解释字符串中的任何标量和特殊意义的字符,应该将字符串用单括号括起来。这时,Perl不解释其中的任何字符,除了\\和\'。最后,可以用(')将命令括起来,这样,其中的命令可以正常运行,并能得到命令的返回值。请看下面的例子。
[root@yangzongde perl]# cat perl2 程序内容 #!/usr/bin/perl #1 $folks="100"; #2 print "\$folks = $folks \n"; #3 print '\$folks = $folks \n'; #4 print "\n\n BEEP! \a \LSOME BLANK \ELINES HERE \n\n"; #5 $date = `date +%D`; #6 print "Today is [$date] \n"; #7 chop $date; #8 print "Date after chopping off carriage return: [".$date."]\n";#9 [root@yangzongde perl]# chmod u+x perl2 程序执行权限 [root@yangzongde perl]# ls perl perl2 [root@yangzongde perl]# ./perl2 运行程序 $folks = 100 \$folks = $folks \n BEEP! some blank LINES HERE Today is [05/07/06 ] Date after chopping off carriage return: [05/07/06] [root@yangzongde perl]#
在此程序中,第3行显示$folks的值。$之前必须使用换码符\,以便Perl显示字符串$folks而不是$folks的值100。
第4行使用的是单引号,结果Perl不解释其中的任何内容,只是原封不动地将字符串显示出来。
第6行使用的是('),则date +%D命令的执行结果存储在标量$date中。
(2)数组。
数组也叫做列表,是由一系列的标量组成的。数组变量以@开头。请看以下的赋值语句。
@food = ("apples","pears","eels"); @music = ("whistle","flute");
数组的下标从0开始,可以使用方括号引用数组的下标。
$food[2]
返回eels。注意@已经变成了$,因为eels是一个标量。
在Perl中,数组有多种赋值方法,例如:
@moremusic = ("organ",@music,"harp"); @moremusic = ("organ","whistle","flute","harp");
还有一种方法可以将新的元素增加到数组中。
push(@food,"eggs");
把eggs增加到数组@food的末端。要往数组中增加多个元素,可以使用下面的语句。
push(@food,"eggs","lard"); push(@food,("eggs","lard")); push(@food,@morefood);
push返回数组的新长度。
pop用来将数组的最后一个元素删除,并返回该元素。例如:
@food = ("apples","pears","eels"); $grub = pop(@food);#此时$grub = "eels"
请看下面的例子。
[root@yangzongde perl]# cat perl3 1 #!/usr/bin/perl 2 # 3 # An example to show how arrays work in Perl 4 # 5 @amounts = (10,24,39); 6 @parts = ('computer','rat',"kbd"); 7 8 $a = 1; $b = 2; $c = '3'; 9 @count = ($a,$b,$c); 10 11 @empty = (); 12 13 @spare = @parts; 14 15 print '@amounts = '; 16 print "@amounts \n"; 17 18 print '@parts = ';
19 print "@parts \n"; 20 21 print '@count = '; 22 print "@count \n"; 23 24 print '@empty = '; 25 print "@empty \n"; 26 27 print '@spare = '; 28 print "@spare \n"; 29 30 31 # 32 # Accessing individual items in an array 33 # 34 print '$amounts[0] = '; 35 print "$amounts[0] \n"; 36 print '$amounts[1] = '; 37 print "$amounts[1] \n"; 38 print '$amounts[2] = '; 39 print "$amounts[2] \n"; 40 print '$amounts[3] = '; 41 print "$amounts[3] \n"; 42 43 print "Items in \@amounts = $#amounts \n"; 44 $size = @amounts; print "Size of Amount = $size\n"; 45 print "Item 0 in \@amounts = $amounts[$[]\n"; [root@yangzongde perl]# chmod u+x perl3 [root@yangzongde perl]# ls perl perl2 perl3 [root@yangzongde perl]# ./perl3 @amounts = 10 24 39 @parts = computer rat kbd @count = 1 2 3 @empty = @spare = computer rat kbd $amounts[0] = 10 $amounts[1] = 24 $amounts[2] = 39 $amounts[3] = Items in @amounts = 2 Size of Amount = 3 Item 0 in @amounts = 10 [root@yangzongde perl]#
在第5行,3个整数值赋给了数组@amounts。第6行,3个字符串赋给了数组@parts。第8行,字符串和数字分别赋给了3个变量,然后将3个变量赋给了数组@count。第11行创建了一个空数组。第13行将数组@spare赋给了数组@parts。第15到第28行输出了显示的前5行。第34到第41行分别存取数组@amounts的每个元素。注意$amount[3]不存在,所以显示一个空项。第43行中使用$#array方式显示一个数组的最后一个下标,所以数组@amounts的大小是($#amounts+1)。第44行中将一个数组赋给了一个标量,则将数组的大小赋给了标量。第45行使用了一个Perl中的特殊变量$ [,用来表示一个数组的起始位置(默认为0)。
(3)数组。
一般的数组允许通过数字下标存取其中的元素。例如,数组food的第一个元素是$food[0],第二个元素是$food[1],以此类推。但Perl允许创建相关数组,这样可以通过字符串存取数组。其实,一个相关数组中每个下标索引对应两个条目,第一个条目叫做关键字,第二个条目叫做数值。这样就可以通过关键字来读取数值。相关数组名以百分号(%)开头,通过花括号({})引用条目。例如:
%ages = ("Michael Caine",39, "Dirty Den",34, "Angie",27, "Willy","21 in dog years", "The Queen Mother",108); 可以通过下面的方法读取数组的值。 $ages{"Michael Caine"};# Returns 39 $ages{"Dirty Den"};# Returns 34 $ages{"Angie"};# Returns 27 $ages{"Willy"};# Returns "21 in dog years" $ages{"The Queen Mother"};# Returns 108
3.6.3 文件句柄和文件操作
可以通过下面的程序了解一下文件句柄的基本用法。此程序的执行结果和UNIX系统的cat命令一样。
#!/usr/local/bin/perl # # Program to open the password file, read it in, # print it,and close it again. $file = '/etc/passwd'; # Name the file open(INFO,$file); # Open the file @lines = <INFO>; # Read it into an array close(INFO); # Close the file print @lines; # Print the array
open函数打开一个文件,其中第一个参数是文件句柄(filehandle)。文件句柄用来标识一个文件。第二个参数指向该文件的文件名。close函数关闭该文件。
如果open函数以写入和追加的方式打开文件,只需分别在文件名之前加上>和> >。
open(INFO,$file);# Open for input open(INFO,">$file");# Open for output open(INFO,">>$file");# Open for appending open(INFO,"<$file");# Also open for input
另外,如果希望输出内容到一个已经打开的文件中,可以使用带有额外参数的print语句。例如:
print INFO "This line goes to the file.\n";
最后,可以使用如下的语句打开标准输入(通常为键盘)和标准输出(通常为显示器)。
open(INFO,'-');# Open standard input open(INFO,'>-');# Open standard output
一个 Perl程序在它一启动时就已经建立3 个标准文件:STDIN(标准输入设备)、STDOUT(标准输出设备)和STDERR(标准错误信息输出设备)。
如果想要从一个已经打开的文件句柄中读取信息,可以使用< > 运算符。
使用read和write函数可以读写一个二进制的文件,其用法如下所示。
read(HANDLE,$buffer,$length[,$offset]);
此命令可以把文件句柄是HANDLE的文件从文件开始位移$ offset处,共$length字节,读到$buffer中。其中$ offset是可选项,如果省略$offset,则read( )从当前位置的前$length个字节读取到当前位置。可以使用下面的命令查看是否到了文件末尾。
eof(HANDLE);
如果返回一个非零值,则说明已经到达文件的末尾。
打开文件时可能出错,所以可以使用die( )显示出错信息。下面打开一个叫做“test.data”的文件。
open(TESTFILE,"test.data") || die "\n $0 Cannot open $! \n";
3.6.4 循环结构
(1)foreach循环。
在Perl中,可以使用foreach循环来读取数组或其他类似列表结构中的每一行。请看下面的例子。
foreach $morsel (@food)# Visit each item in turn # and call it $morsel { print "$morsel\n";# Print the item print "Yum yum\n";# That was nice }
每次要执行的命令用花括号括出。第一次执行时$morsel被赋予数组@food的第一个元素的值,第二次执行时$morsel被赋予数组@food的第二个元素的值,以此类推直到数组的最后一个元素。
(2)判断运算。
在Perl中任何非零的数字和非空的字符串都被认为是真。零、全为零的字符串和空字符串都为假。
下面是一些判断运算符。
$a == $b 如果$a 和$ b相等,则返回真 $a != $b 如果$ a和$ b不相等,则返回真 $a eq $b 如果字符串$ a和字符串$ b相同,则返回真 $a ne $b 如果字符串$ a和字符串$ b不相同,则返回真
可以使用逻辑运算符:
($a && $b) $ a与$ b ($a || $b)$a 或$ b ! ( $ a ) 非$ a
(3)for循环。
Perl中的for结构和C语言中的for结构基本一样。
for (initialise; test; inc) { first_action; second_action; etc }
下面是一个for循环的例子,用来显示从0到9的数字。
for ($i = 0; $i < 10; ++$i)# Start with $i = 1 # Do it while $i < 10 # Increment $i before repeating { print "$i\n"; }
(4)while和until循环。
下面是一个while和until循环的例子。它从键盘读取输入直到得到正确的口令为止。
#!/usr/local/bin/perl print "Password? ";# Ask for input $a = <STDIN>;# Get input chop $a;# Remove the newline at end while ($a ne "fred")# While input is wrong... { print "sorry. Again? ";# Ask again $a = <STDIN>;# Get input again chop $a;# Chop off newline again }
当输入和口令不相符时,执行while循环。也可以在执行体的末尾处使用while和until,这时需要用do语句。
#!/usr/local/bin/perl do { "Password? "; # Ask for input $a = <STDIN>; # Get input chop $a; # Chop off newline } while ($a ne "fred") # Redo while wrong input
3.6.5 条件结构
Perl也允许使用if/then/else表达式。请看下面的例子。
if ($a) { print "The string is not empty\n"; } else { print "The string is empty\n"; }
注意在Perl中,空字符被认为是假。If结构中也可以使用嵌套结构。
if (!$a)# The ! is the not operator { print "The string is empty\n"; } elsif (length($a) == 1)# If above fails,try this { print "The string has one character\n"; } elsif (length($a) == 2)# If that fails,try this { print "The string has two characters\n"; } Else # Now,everything has failed { print "The string has lots of characters\n"; }
3.7 本章总结
本章详细介绍了Linux程序设计的基础知识,包括如何建立嵌入式Linux交叉编译环境、了解和使用工程管理器make,以及LinuxC/C++程序、汇编程序、Shell编程、Perl编程的基础,几乎包括了所有的 Linux程序设计内容。通过本章的学习,读者将了解 Linux程序设计的编译和管理环境,熟悉各类程序设计语言特点,为后面的实例学习打下坚实的基础。