43、编程工具介绍

编程工具介绍

1. 共享库问题与 LD_LIBRARY_PATH 变量

在编程过程中,共享库可能会带来诸多问题,而环境变量 LD_LIBRARY_PATH 是这些问题的首要根源。当我们在 LD_LIBRARY_PATH 中写入以冒号分隔的目录列表时, ld.so 程序会先在这些目录中搜索库,再去其他地方查找。这是一种在无法访问程序源代码或不想重新编译程序时,确保程序正常运行的低成本方法。

然而,这种方法存在弊端。绝对不能在系统启动文件或程序编译时设置 LD_LIBRARY_PATH 的值。因为动态链接器每次找到这个变量时,都需要多次搜索其中指定的每个目录,这不仅会严重降低系统性能,还可能导致库冲突和错误的链接,因为这些目录会为系统中运行的每个程序进行搜索。

如果必须使用 LD_LIBRARY_PATH 来运行没有源代码的程序(如 Mozilla 等不想编译的应用程序),建议使用包装脚本。例如,若程序 /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 可以避免大部分共享库问题。但有时,程序员还会遇到另一个重要问题,即库的 API 变更可能导致已安装程序出错。预防此类问题,可采用一致的方法安装共享库,如使用 -Wl,-rpath 选项在运行环境中创建链接路径,或仅使用静态库版本。

2. make 工具

对于由多个源文件组成或需要特殊编译选项的程序,手动编译十分不便。为解决这一问题,Unix 系统中出现了标准的 make 工具,用于管理编译过程。许多系统工具都依赖 make ,所以在使用 Unix 系统时,有必要了解它。

make 工具虽庞大,但原理相对简单。若在程序目录中看到 Makefile makefile 文件,就意味着可以使用 make 系统。通过执行 make 命令,可尝试编译程序。

make 系统的核心是目标(target),它可以是文件(如 .o 文件、可执行文件等)或标签。有些目标可能依赖于其他目标,如创建可执行文件前需准备好所有目标文件,这些被依赖的目标称为依赖项(dependencies)。构建目标时, make 会遵循规则(rule),例如将 .c 源文件转换为 .o 目标文件。 make 自身有一些内置规则,也可自定义。

2.1 示例 Makefile 文件

以下是一个简单的 Makefile ,用于根据 main.c aux.c 源文件构建名为 myprog 的程序:

# 目标文件
OBJS=aux.o main.o 
all: myprog 
myprog: $(OBJS)
        $(CC) -o myprog $(OBJS)
  • # 开头的行是注释。
  • OBJS 是一个宏定义,将两个目标文件的名称赋值给它,后续可通过 $(OBJS) 引用。
  • all 是默认目标,执行 make 命令时会默认执行它。
  • all 目标依赖于 myprog 目标,而 myprog 目标依赖于 $(OBJS) 展开后的 aux.o main.o 文件。

执行 make 命令时,会显示以下命令:

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

下面是该 Makefile 的依赖关系图:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    A(all):::process --> B(myprog):::process
    B --> C(aux.o):::process
    B --> D(main.o):::process
    C --> E(aux.c):::process
    D --> F(main.c):::process
2.2 内置规则

make 系统如何知道将 aux.c 转换为 aux.o 呢?这是因为 make 有内置规则。当需要使用 .o 文件时,它会自动根据 .c 文件创建,并知道如何正确调用 cc 编译器进行转换。

2.3 程序的最终构建

构建 myprog 程序的最后一步,是在准备好 $(OBJS) 中的两个目标文件后,执行以下命令( $(CC) 会被替换为编译器名称):

        $(CC) -o myprog $(OBJS)

需要注意的是,该行命令前必须有一个制表符,每个命令需单独占一行。若出现类似 Makefile:7: *** missing separator. Stop. 的错误,说明 Makefile 文件可能损坏,因为制表符是命令分隔符,缺失或其他错误会导致该问题。

2.4 更新机制

make 系统的最后一个原则是目标应随其依赖项更新。例如,连续两次执行 make 命令,第二次会显示 make: Nothing to be done for ‘all’. ,因为 make 检查规则后发现 myprog 文件已存在,且自上次调用以来依赖项未改变。可进行如下小实验:
1. 执行 touch aux.c 命令更新 aux.c 文件的时间戳。
2. 再次执行 make 命令, make 会发现 aux.c aux.o 新,因此会重新编译 aux.c
3. 由于 myprog 依赖于 aux.o ,而 aux.o 现在比 myprog 新,所以 make 会重新创建 myprog 。这是 make 系统中常见的链式反应示例。

2.5 命令行参数和选项

了解 make 系统的命令行参数和选项,可让它完成很多有用的工作:
- 指定目标名称:在命令行中指定目标名称,如 make aux.o ,可仅创建 aux.o 文件。
- 定义宏:可在命令行中定义宏,如 make CC=clang ,让 make 使用 clang 编译器而非默认的 cc 编译器。这在测试预处理器定义或库时特别有用,尤其是 CFLAGS LDFLAGS 宏。
- 不使用 Makefile :若 make 的内置规则足以构建目标,可直接执行 make 命令并指定目标名称。例如,对于简单的 bla.c 源文件,执行 make bla 命令, make 会尝试执行 cc bla.o -o bla 。但这种方式仅适用于最简单的 C 语言程序,若程序需要库或特殊的头文件目录,必须编写 Makefile 。不使用 Makefile 执行 make 命令,在处理非 C 语言程序(如 Fortran、lex 或 yacc)且不清楚编译器或工具如何工作时很有用,即使 make 无法完成目标,也可能提供足够的提示。

此外,还有两个常用选项:
- -n :打印构建目标所需的命令,但不执行。
- -f 文件名 :让 make 从指定文件而非 Makefile makefile 中读取数据。

2.6 标准宏和变量

make 系统使用许多特殊的宏和变量,本书统一用宏来描述构建目标过程中不改变的内容。常见宏如下:
| 宏名称 | 描述 |
| ---- | ---- |
| CFLAGS | C 编译器选项,构建 .c 文件的目标代码时传递给编译器 |
| LDFLAGS | 类似 CFLAGS ,但用于存储创建可执行文件时传递给链接器的选项 |
| LDLIBS | 若不想将库名选项与 LDFLAGS 中的搜索路径合并,可将库名选项放在此宏中 |
| CC | C 编译器名称,默认为 cc |
| CPPFLAGS | C 预处理器选项, make 调用预处理器时会传递这些选项 |
| CXXFLAGS | GNU make 用于存储 C++ 编译器选项的宏 |

make 系统的变量在构建过程中可能会改变,以下是常见变量:
| 变量名称 | 描述 |
| ---- | ---- |
| $@ | 在规则内部,该变量表示目标名称 |
| $* | 该变量展开为当前目标的基本名称,如创建 bla.o 目标时,它存储 bla 名称 |

需注意,GNU make 有许多扩展、内置规则和功能,在 Linux 系统中使用没问题,但移植到 Sun 或 BSD 系统时可能会有意外情况。

2.7 常见编译目标

大多数 Makefile 包含标准目标,用于完成与整个程序编译相关的部分任务:
- clean :几乎所有 Makefile 都有该目标,执行 make clean 通常会删除所有目标文件和可执行文件,以便重新开始编译。例如,为 myprog 程序的 Makefile 添加如下规则:

clean:
        rm -f $(OBJS) myprog
  • distclean :使用 GNU autotools 创建的 Makefile 通常有此目标,会删除所有非原始发行版的内容,包括 Makefile 本身。某些情况下,程序员可能选择使用 realclean 目标而不删除可执行文件。
  • install :该目标会将编译好的程序和文件复制到系统中指定的安装位置。此操作有风险,建议先执行 make -n install 检查实际安装内容。
  • test check :部分 Makefile 会添加此目标,用于检查程序编译后是否正常运行。
  • depend :该目标通过特殊的 -M 选项调用编译器来创建依赖关系,可能会修改 Makefile 本身。虽不常用,但遇到要求调用此目标的程序时,必须执行。
  • all :始终是 Makefile 的第一个目标,很多文档描述的是该目标而非可执行程序本身。
2.8 Makefile 文件的组织

创建 Makefile 虽有多种风格,但通常遵循一些通用原则:
- 宏定义部分:将库和头文件选项按不同包分组,例如:

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) $(X_INCLUDES) $(PNG_INCLUDES) 
LDFLAGS=$(LDFLAGS) $(X_LIB) $(PNG_LIB)
  • 目标文件分组:通常根据可执行文件分组目标文件。假设要创建 boring trite 两个可执行文件,每个文件有自己的 .c 文件且都依赖 util.c 文件, Makefile 可如下编写:
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)

将两个可执行文件合并到一个目标不太实用,因为可能会阻碍规则迁移到其他 Makefile ,或删除单个可执行文件或一组文件。而且这种依赖关系不真实,若 Makefile 中只有一个规则处理两个程序,会导致 boring 依赖 trite.c trite 依赖 boring.c ,重新构建一个程序时需重新构建两个程序。

若需为目标文件准备特殊规则,应将其放在构建可执行程序规则之前。若多个可执行文件使用同一个目标文件,规则应放在所有相关程序规则之前。

3. 调试器

在 Linux 系统中,标准的调试器是 gdb ,也有用户友好的界面,如 Eclipse IDE 和 Emacs。为了能对程序进行全面调试,编译程序时需向编译器传递 -g 选项,让编译器在可执行文件中保存符号表和调试器所需的其他信息。

使用以下命令启动 gdb 调试器调试 program 程序:

$ gdb program

屏幕会显示 (gdb) 提示符,在调试器命令行中输入以下命令,可启动加载的 program 程序并传递选项:

(gdb) run opcje

若程序无问题,会正常运行并结束;若出现问题,调试器会停止程序,打印调用栈和导致问题的部分代码,并返回 (gdb) 命令行。代码片段可能提示问题原因,还可使用 print 命令打印变量值(该命令也适用于数组和 C 语言结构体),以更精确地找出错误原因:

(gdb) print zmienna

断点(breakpoint)功能可在程序任意位置暂停执行。使用以下命令设置断点,其中 plik 是源文件名称, numer_wiersza 是文件中的行号:

(gdb) break plik:numer_wiersza

使用以下命令让调试器继续执行程序:

(gdb) continue

使用以下命令删除断点:

(gdb) clear plik:numer_wiersza

综上所述,掌握这些编程工具和调试器的使用方法,能有效提高编程效率和解决问题的能力。在实际开发中,根据具体需求灵活运用这些工具,可更好地完成程序的编译、调试和维护工作。

编程工具介绍(续)

4. 工具综合运用案例

为了更好地理解上述编程工具的使用,我们来看一个综合运用的案例。假设我们要开发一个简单的 C 语言项目,包含多个源文件,并且需要使用一些外部库,同时要进行调试和编译管理。

4.1 项目需求与结构

我们的项目名为 myproject ,包含以下源文件和需求:
- main.c :项目的主程序入口。
- utils.c :包含一些通用的工具函数。
- 需要链接 math.h 库进行数学计算。
- 要对代码进行调试。

项目的目录结构如下:

myproject/
├── main.c
├── utils.c
├── utils.h
└── Makefile
4.2 编写 Makefile 文件

根据前面介绍的 Makefile 编写原则,我们来创建 Makefile 文件:

# 宏定义
CC = gcc
CFLAGS = -g -Wall -I.
LDFLAGS = -lm

# 目标文件
OBJS = main.o utils.o

# 默认目标
all: myproject

# 生成可执行文件
myproject: $(OBJS)
        $(CC) -o myproject $(OBJS) $(LDFLAGS)

# 生成目标文件
%.o: %.c
        $(CC) $(CFLAGS) -c $< -o $@

# 清理目标
clean:
        rm -f $(OBJS) myproject
  • CC :指定使用 gcc 作为编译器。
  • CFLAGS :添加 -g 选项用于调试, -Wall 显示所有警告信息, -I. 指定当前目录为头文件搜索路径。
  • LDFLAGS :添加 -lm 链接数学库。
  • OBJS :定义目标文件列表。
  • all :默认目标,生成 myproject 可执行文件。
  • %.o: %.c :通用规则,将 .c 文件编译为 .o 文件。
  • clean :清理目标,删除所有目标文件和可执行文件。
4.3 编译项目

在项目根目录下执行 make 命令, make 工具会根据 Makefile 的规则进行编译:

$ make
gcc -g -Wall -I. -c main.c -o main.o
gcc -g -Wall -I. -c utils.c -o utils.o
gcc -o myproject main.o utils.o -lm
4.4 调试项目

编译完成后,若发现程序有问题,可使用 gdb 进行调试。启动 gdb 调试 myproject 程序:

$ gdb myproject

gdb 提示符下,可设置断点、运行程序、打印变量等操作。例如,在 main.c 的第 10 行设置断点,然后运行程序:

(gdb) break main.c:10
(gdb) run

程序会在第 10 行暂停,此时可使用 print 命令查看变量值:

(gdb) print some_variable
5. 工具使用的注意事项

在使用上述编程工具时,有一些注意事项需要牢记:
- LD_LIBRARY_PATH 变量 :尽量避免使用,若必须使用,使用包装脚本,防止系统性能下降和库冲突。
- Makefile 文件
- 命令前必须使用制表符,否则会导致错误。
- 合理组织 Makefile ,将相关选项和目标文件分组,便于维护。
- 注意目标的依赖关系,避免出现不必要的重新编译。
- 调试器
- 编译时添加 -g 选项,确保调试信息完整。
- 合理使用断点和打印变量功能,快速定位问题。

6. 总结

本文介绍了编程过程中常用的工具,包括共享库问题与 LD_LIBRARY_PATH 变量的处理、 make 工具的使用、调试器 gdb 的操作,以及工具的综合运用案例和注意事项。通过掌握这些工具,程序员可以更高效地进行程序的编译、调试和维护工作。

以下是这些工具的使用流程总结:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    A(编写代码):::process --> B(创建 Makefile):::process
    B --> C(编译程序):::process
    C --> D{程序是否正常运行?}:::process
    D -- 是 --> E(程序发布):::process
    D -- 否 --> F(使用 gdb 调试):::process
    F --> G(修改代码):::process
    G --> B

在实际开发中,根据项目的需求和复杂度,灵活运用这些工具,不断积累经验,提高编程技能和解决问题的能力。希望本文能为广大程序员在编程工具的使用上提供有益的参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值