Linux——开发工具

开发工具

C编译器

大多数的Linux工具,以及很多Linux应用软件,都是用C或C++写成的。本章主要以C作为例子,但你把这些概念搬到C++上是没问题的。
C程序遵照传统的开发流程:写代码、编译代码、运行代码。也就是说,想让写好的C代码能运行起来,你必须先将其编译成计算机能理解的、低层次的二进制形式。你可拿C来跟后面讲到的一些脚本语言作对比,它们不需要编译。

大多数发行版都默认不含有编译C的工具,因为它们比较占空间。如果你发现没有这些工具,对于Debian/Ubuntu,你可以安装build-essential包,而对于Fedora/CentOS,则可以用yum groupinstall。如果不行,就试着找一下“C compiler”

虽然来自LLVM项目的新的C编译器clang越来越流行,但在大部分Unix系统上通行的C编译器还是GNU C编译器gcc。C源码的文件名以.c结尾。现在来看一个叫hello.c的独立的C源码文件

#include 
main() {
    printf("Hello, World.\n");
}

将以上代码置于一个叫hello.c的文件中,然后执行以下命令:

$ cc hello.c

这样会产生一个叫a.out的可执行文件,你可以像运行其他可执行文件一样运行它。

$ cc -o hello hello.c

多个源码文件

大部分C程序都写得很大,不宜放到一个单独的源码文件中。大文件不便于程序员管理,编译时也可能会出错。因此,开发者会将代码分块放在多个文件中。

我们不会将这些.c文件马上编译成可执行文件,而是使用编译器的-c选项来给每个文件生成对应的对象文件。假设你有main.c和aux.c两个文件,以下两条编译器命令会完成建立程序的大部分工作:

$ cc -c main.c
$ cc -c aux.c

以上两命令会从这两个文件编译出main.oaux.o两个对象文件。

对象文件是一种二进制文件,除了个别地方,处理器差不多能解读它。首先,操作系统是不知道如何运行对象文件的;其次,你可能会需要将一些对象和系统库组合成一个完整的程序。

要从一个或多个对象文件建立一个功能完整的可执行程序,你需要用连接器,如Unix中的ld命令。但程序员是很少在命令行上用它的,因为C编译器包含了该步骤。所以,想从上面两个对象文件建立myprog的话,就用以下命令来连接它们:

$ cc -o myprog main.o aux.o

头(include)文件和目录

C的头文件是用于保存类型和函数声明的附加文件。

不幸的是,有很多编译问题是与头文件有关的。大多数情况下是因为编译器找不到头文件或库。有些情况下是因为程序员忘了包含必要的头文件,导致某些代码编译不通过。

修复include文件的问题

记住正确的include文件并不总是易事。有时相同名字的include文件放在了不同的目录中,令人难以分辨哪个才是需要的。当编译器找不到include文件时,就会出现类似这样的报错信息:

badinclude.c:1:22: fatal error: notfound.h: No such file or directory

该信息说明了badinclude.c文件需要参考的notfound.h头文件无法找到。这个错误源自badinclude.c的第一行:

#include 

Unix默认的include目录是/usr/include,编译器一般就看那里,除非你指定它看别的地方。(大多数包含头文件的路径都会带有include的名字。)

假设notfound.h在/usr/junk/include中,你可以加上-I选项,让编译器看到这个目录:

$ cc -c -I /usr/junk/include badinclude.c

现在就不会因为头文件的引用出错而不能编译了。

另外,你还要注意include中双引号("")与尖括号(<>)的区别:

#include "myheader.h"

双引号意味着头文件不在系统的include目录中,需要编译器从其他地方寻找。这通常表示它与源码文件处于同一目录中。如果你使用双引号时出现问题,可能是你要编译的程序并不完整。

什么是C预处理器

其实,并不是C编译器去寻找include文件,而是C预处理器(C Preprocessor,简称cpp)。它是编译器在解析程序之前先在源码上运行的一个东西。预处理器会将源码重写成一种编译器能理解的形式,它能使源码更易读(并提供捷径)。

源码中的预处理器命令叫作指令(directive),它们以#开头,分为以下三种。

  • include文件:#include指令会使预处理器将整个文件包含进来。例如在上节中提到的,是编译器的-I选项让预处理器在指定目录中搜索include文件。
  • 宏定义:#define BLAH something这样的一行,会使预处理器将源码中所有的BLAH替换成something。一般我们约定宏的名字都是大写的,但有人会将宏的名字起得像函数名或变量名。
  • 条件:你可以用#ifdef、#if和#endif来对代码进行分块。#ifdef MACRO指令用于检查宏MACRO是否已定义,而#if condition则检查condition是否非零。当预处理器发现if语句后的条件为false时,它就不会将#if和#endif之间的代码交给编译器。如果你想看懂C程序,最好先习惯这种指令。

下面有一个条件指令的例子。当预处理器遇到这段代码时,它会检查宏DEBUG是否已定义。如果是,它就会将fprintf()那行交给编译器,否则,就跳过该行,继续处理#endif后的代码。

#ifdef DEBUG
	fprintf(stderr, "This is a debugging message.\n");
#endif

Unix上的C预处理器是cpp,你也可以用gcc -E来运行它。不过你一般很少需要单独运行预处理器。

连接库

所谓C库,就是一些已编译好的、通用的、可让你添加到自己程序的函数。例如,许多可执行程序都会用到数学库,因为其中包含了三角函数之类的东西。

库主要是在连接的时候(即连接器从对象文件产生可执行程序之时)发挥作用。比如说,你有一个需要用到gobject库的程序,但你忘了告诉编译器你需要连接它,那么你就会看到这样的错误信息:

badobject.o (.text+0x28): undefined reference to 'g_object_new'

这条报错信息的关键是g_object_new部分。当连接器检查badobject.o这个对象文件时,发现找不到g_object_new这个函数,于是就无法创建程序了。对于这个例子,你可以怀疑是你自己忘记加上gobject库了,因为错误信息说找不到的函数是g_object_new()。

要解决这个问题,首先你要知道gobject库在哪里,然后再用编译器的-l选项连接它。跟include文件相似,库分布在系统的各个地方(默认是在/usr/lib中),大多数会放在名为lib的子目录中。而对于上例,基本的gobject库文件是libgobject.a,而库的名字是gobject。所以完整的连接和编译应是这样:

$ cc -o badobject badobject.o -lgobject

如果库的所在地不在常规位置,你必须用-L选项来告诉连接器。假设badobject程序需要用到/usr/junk/lib中的libcrud.a,那么就应该这样编译:

$ cc -o badobject badobject.o -lgobject -L/usr/junk/lib -lcrud

共享库

名称以.a结尾的库(如libgobject.a)是静态库。当程序连接的是静态库时,连接器会将库文件中的机器码复制到你的程序中。于是,最终的可执行程序不需要该库也能运行起来,并且其所引用到的行为将不会改变。

然而,库是会一直变大的,就如同库会变多一样,所以复制静态库很浪费磁盘和内存空间。另外,如果某天你发现所用的静态库有问题,需要修改或替换,那些引用到它的程序就都要重新编译,才能使用到新的库。

引用共享库的程序只会在需要时才将该库加载到内存中。而且多个进程可以共享内存中的同一个共享库。修改共享库的代码不需要重编译那些引用它的程序。

使用共享库的代价是管理困难、连接复杂。

需要注意的事情:

  1. 如何列出程序需要的共享库;
  2. 程序如何查找共享库
  3. 如果让程序连接共享库
  4. 常见的共享库陷阱

列出共享库的依赖关系

共享库与静态库通常放在同一个地方。Linux的两大标准库目录是/lib和/usr/lib。其中/lib是不应该包含静态库的。

共享库的名字的后缀中通常含有.so(意为共享对象)

$ ldd /bin/bash
linux-gate.so.1 => (0xb7799000)
libtinfo.so.5 => /lib/i386-linux-gnu/libtinfo.so.5 (0xb7765000)
libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb7760000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75b5000)
/lib/ld-linux.so.2 (0xb779a000)

考虑到最佳性能和灵活性,可执行程序本身通常是不知道它所用的共享库的所在位置的。它只知道共享库的名字,或只知道一点寻找共享库的提示。ld.so这个小程序(它是运行时动态连接器/加载器)可以为程序在运行时找到并加载共享库。以上ldd的输出中,左边是可执行程序所知道的库的名字,右边是ld.so所找到的库的位置。

ld.so怎样找到共享库

共享库的一个常见问题是动态连接器无法找到库。如果可执行程序有预先配置好的运行时库搜索路径(runtime library search path,以下简称rpath)的话,则动态连接器一般会首先查找那里。很快你就会看到怎样创建这个路径。

接着,动态连接器就会参考系统缓存/etc/ld.so.cache,看看该库是否在常规的位置。这是从缓存配置文件/etc/ld.so.conf中的目录列表获取的库文件名字的快速缓存。

在ld.so.conf里,每一行就是一个你要包含到缓存里的目录。这个列表通常很短,内容类似这样:

/lib/i686-linux-gnu
/usr/lib/i686-linux-gnu

标准的库目录/lib和/usr/lib是隐式的,即你不需要在/etc/ld.so.conf中包含它们。
如果你改动了ld.so.conf或者改变了某个共享库的目录,你都必须通过以下命令来手动重建/etc/ld.so.cache文件:

# ldconfig -v

-v选项会输出被ldconfig添加到缓存的目录的详细信息和它所监测到的改动。
ld.so找共享库时还会参考一个地方:环境变量LD_LIBRARY_PATH

把程序与共享库连接起来

假设你在/opt/obscure/lib中有一个叫libweird.so.1的动态库,并需要把它与myprog连接,可以这么做:

$ cc -o myprog myprog.o -Wl,-rpath=/opt/obscure/lib -L/opt/obscure/lib -lweird

-Wl-rpath用于告诉连接器将某个目录包含到程序的rpath中。虽然写了-Wl、-rpath,但还是需要加上-L。

对于已编译的程序,可以使用patchelf来加入不同的rpath,不过最好还是在编译时就做好。

共享库的一些问题

常见的问题:

  • 找不到库
  • 性能低
  • 找错库

出现共享库问题的头号原因来自环境变量LD_LIBRARY_PATH。把一些以冒号分隔的目录赋值到这个变量的话,就可令ld.so首先查找这些目录。如果你没有源码,或不能用patchelf,又或者你只是不想重新编译,你都可以用这招快速解决库移动后的依赖问题。但这可能会造成混乱。

永远都不要在启动文件中或在编译软件时设置LD_LIBRARY_PATH。动态运行时连接器看到这个变量时,就会对其中设定的目录进行库的搜索。这不仅会造成性能低下,而且更重要的是,它可能会导致库混淆,因为运行时连接器会为每一个程序到这些目录中查找库。

如果有一些没有源码的无足轻重的程序(又或者是一些你不想重编译的应用,如Mozilla等)迫使你用LD_LIBRARY_PATH来解决库依赖问题,那就将它嵌套进脚本里。假设你有一个可执行程序/opt/crummy/bin/crummy.bin,它需要/opt/crummy/lib中的共享库,你可以写一个类似这样的crummy嵌套脚本:

#!/bin/sh
LD_LIBRARY_PATH=/opt/crummy/lib
export LD_LIBRARY_PATH
exec /opt/crummy/bin/crummy.bin $@

不使用LD_LIBRARY_PATH能避免大部分共享库问题。但开发者可能还会遇到一个偶尔出现的大问题,就是库的应用程序接口(Application Programming Interface,以下简称API)改变了,使得装好的软件都用不了。最好的解决方法就是预防。具体做法是:安装库时也使用-Wl、-rpath,或者使用静态库。

make

make是一个很大的系统,但它并不难理解。当看到有叫Makefile或makefile的文件时,就说明你遇上make了。(试着运行make看你能构建什么东西。)

make背后的理念就是目标,即你想达到的目的。目标可以是文件(一个.o文件或一个可执行
文件等等)或者标签。另外,有些目标是依赖于其他目标的。举例来说,在连接之前,你得先做好一堆.o文件。这种需求就是依赖关系。

make会根据一些规则(例如怎样把.c文件变成.o文件)来构建目标。make本身就有一些规则,但你也可以修改它,或增加自己的规则。

一个Makefile实例

看一下这个简单的Makefile,它会将aux.c和main.c构建成一个叫myprog的程序:

# object files
OBJS=aux.o main.o
all: myprog
myprog: $(OBJS)
$(CC) -o myprog $(OBJS)

第一行开头的#表示该行是注释。

把OBJS赋值为两个对象文件,这对于后续的操作很重要。是宏定义。

是第一个目标all。第一个目标就是运行make后所要达到的最终结果。

构建目标的规则写在冒号后面。对于all来说,就是满足一个叫myprog的东西

为了构建myprog,这个Makefile在依赖关系中使用了$(OBJS)。该宏展开成aux.o和main.o,于是myprog就依赖于这两个文件了(它们必须是文件,因为该Makefile中没有其他名为aux.o或main.o的目标)。

该Makefile假设你有两个叫aux.c和main.c的C源码文件与其在同一目录中。执行make的话,就
会产生以下输出,其中显示了make所运行的命令:

$ make
cc -c -o aux.o aux.c
cc -c -o main.o main.c
cc -o myprog aux.o main.o

内置规则

make有自己内置的规则。当你需要.o文件时,它就会自动去找.c文件,它甚至懂得对那些.c文件运行cc -c命令,以达到获得.o文件的目标。

最终的程序构建

创建myprog的最后一步有点复杂,但其思想是很简单的。$(OBJS)有了两个对象文件之后。

你就可以按照最后一行来运行C编译器了(这里$(CC)展开成编译器的名字):

$(CC) -o myprog $(OBJS)

$(CC)前的空格是tab键。任何真实的命令前面都必须要有tab键。小心以下这种情况:

Makefile:7: *** missing separator. Stop.

这种错误是说Makefile损坏了。tab键就是分隔符。如果没有分隔符,或者出现其他干扰,你就会看到这样的错误提示。

保持更新

make的基础知识还有最后一点,就是目标需要跟它的依赖关系一同更新。如果你对上例make两次,那么第一次会构建出myprog,而第二次则会给出这样的信息:

make: Nothing to be done for 'all'.

在第二次时,make会发现规则中的myprog已经存在,而且自从上次构建之后,所有依赖关系都未曾改变,于是它就不会再次构建myprog。要解决这个问题,可以touch一个文件,更改文件的修改时间。然后就可以更新了。典型的反应链。

命令行参数与选项

最有用的选项之一就是在命令行上指定一个单独的目标。对于上面例子,如果你只想得到aux.o的话,可以执行make aux.o。

你也可以在命令行定义一个宏。例如,想使用clang编译器的话

$ make CC=clang

make就会使用你定义的CC来取代原本的编译器cc。命令行宏对于测试预处理器定义和库是很有用的,尤其是有CFLAGS和LDFLAGS时。

实际上,运行make不一定要有Makefile。如果内置的make规则能完成目标,你可以直接叫make去执行目标。

这种make只适用于最简单的C程序。如果你的程序需要用到库,或者需要特别的include目录,那你还是应该写个Makefile。在你不了解编译器的运作原理,而又想编译一些例如Fortran、Lex、Yacc之类的东西时,确实可以直接make而不用Makefile。

make还有以下两个好用的选项。

  • -n:显示一次构建所要用到的命令,但并不执行它们。
  • -f file:告诉make使用Makefile和makefile以外的文件。

标准宏和变量

make有很多特别的宏和变量。宏和变量的区别很难说清,我们会把make启动以后不再改动的东西叫作宏。

常用的宏:

  • CFLAGS:C编译器选项。make会将这个选项作为参数,在将.c文件变成.o的阶段传给编译器。
  • LDFLAGS:类似CFLAGS,不过它是在将.o变成可执行程序的阶段传给连接器。
  • LDLIBS:如果你用了LDFLAGS,但不想库名选项与查找路径混在一起,可以将库名选项写在这里。
  • CC:C编译器。默认是cc
  • CPPFLAGS:C预处理器选项。make运行预处理器时,将其作为参数。
  • CXXFLAGS:GNU使用这个宏作为C++编译器选项。

make变量会随着目标的构建而改变。因为我们永远都只是在使用make变量,而不会去手动设置它们,所以把它们都带上$就好了。

  • $@:写在规则里时,表示当前目标。
  • $*:当前目标的基名。例如,在构建blah.o时,它表示为blah。

常规的目标

大部分的Makefile都包含了一些用于辅助编译的常规目标

  • clean:这个目标无处不在。make clean通常会把所有对象文件和可执行程序都清掉,以便你重新构建或者打包软件。
  • distclean:GNU autotools所生成的Makefile总会有这个目标。它能删除原包以外的所有东西,包括Makefile。
  • install:将文件和编译好的程序放到Makefile认为适当的地方。这可能会有风险,所以最好还是先用make -n install看看会放在哪里。
  • test或check:有些开发者会加上test或check目标来检验构建出的东西是否可用。
  • depend:通过编译器的-M选项来检查源码,以建立依赖关系。这是一个不寻常的目标,因为它经常会改动Makefile自身。这已经不是一种通用的做法了,但如果你遇到要求你这么做的情况,那最好还是照着去做。
  • all:通常是Makefile的第一个目标

组织一个Makefile

在Makefile的第一部分(宏定义)定义好不同用途的库和include:

MYPACKAGE_INCLUDES=-I/usr/local/include/mypackage
MYPACKAGE_LIB=-L/usr/local/lib/mypackage -lmypackage
PNG_INCLUDES=-I/usr/local/include
PNG_LIB=-L/usr/local/lib -lpng

于是各种编译器和连接器的选项就由这些宏组合而成:

CFLAGS=$(CFLAGS) $(MYPACKAGE_INCLUDES) $(PNG_INCLUDES)
LDFLAGS=$(LDFLAGS) $(MYPACKAGE_LIB) $(PNG_LIB)

例如,假设你的包会创建出名为boring和trite的可执行程序,它们有各自的.c文件,并且都需要用到util.c,则可以这样定义:

UTIL_OBJS=util.o
BORING_OBJS=$(UTIL_OBJS) boring.o
TRITE_OBJS=$(UTIL_OBJS) trite.o
PROGS=boring trite
all: $(PROGS)
boring: $(BORING_OBJS)
$(CC) -o $@ $(BORING_OBJS) $(LDFLAGS)
trite: $(TRITE_OBJS)
$(CC) -o $@ $(TRITE_OBJS) $(LDFLAGS)

调试器

Linux上标准的调试器是gdb。除此之外,界面友好的Eclipse IDE和Emacs系统也是Linux支持的。

$ gdb program

接着你会得到一个(gdb)提示符,然后可以这样传递命令行参数(假设参数为options):

(gdb) run options

如果程序是可行的,它就会正常启动、运行、退出。而如果有问题的话,gdb就会停止,打印出错误的代码,并回到(gdb)提示符。因为打印出的代码通常就是问题的所在,所以可能需要将其中的变量也打印出来。(print命令也适用于数组和C结构体。

(gdb) print variable

想要gdb在某段代码上暂停,可以使用断点功能。以下例子中,file是源码文件,line_num是需要暂停的位置

(gdb) break file:line_num

想让gdb继续往下执行,可这样做:

(gdb) continue

想清除断点,输入:

(gdb) clear file:line_num

脚本语言

所有脚本语言的第一行是跟Bourne shell的shebang类似的。例如,Python脚本的第一行大概是这样的:

#!/usr/bin/python

或者这样:

#!/usr/bin/env python

在Unix中,所有以#!开头的可执行文本文件都是脚本。其后的路径是该脚本的解释器。当Unix尝试运行以#!开头的可执行文件时,它会启动#!后的解释器,并将文件剩余的内容作为该解释器的标准输入。所以,下例也是一个脚本:

#!/usr/bin/tail -2
This program won't print this line,
but it will print this line...
and this line, too.

你可能感兴趣的:(计算机基础)