Unix/Linux系统编程-学习笔记-第二章 第2章 编程背景

2.1 Linux中的文本编辑器

2.1.1 vim

vim(Linux Vi和Vim Editor 2017)是Linux的标准内置编辑器。它是Unix原始默认vi编辑器的改进版本。与其他大多数编辑器不同,vim有3种不同的操作模式,分别是

  • 命令模式:用于输入命令
  • 插入模式:用于输入和编辑文本。
  • 末行模式:用于保存文件并退出。

vim启动时,处于默认的命令模式,在该模式下,大多数键表示特殊命令。移动光标的命令键示例如下:

  • h:将光标向左移动一个字符
  • l:将光标向右移动一个字符
  • j:将光标向下移动一个字符
  • k:将光标向上移动一个字符

在X-window中使用vim时,也可以通过箭头键来完成光标的移动。要输入文本进行编辑,用户必须输入i(插入)或a(追加)命令将vim切换到插入模式

  • i:切换到插入模式,插入文本。
  • a:切换到插入模式,追加文本。
    要退出插入模式,请按ESC键一次或多次。在命令模式下,输入“:”进入末行模式,将文本保存为文件或退出vim:
  • :w:写入(保存)文件。
  • :q:退出vim。
  • :wq:保存并退出。
  • :q!:不保存更改,强制退出。
    虽然许多Unix用户已经习惯了vim不同的操作模式,但是其他用户可能认为与其他基于图形用户界面(GUI)的编辑器相比,vim使用起来既不自然也不方便。以下类型的编辑器属于通常所说的所见即所得(WYSIWYG)编辑器。在WYSIWYG编辑器中,用户可以输入文本,用箭头键移动光标,和普通的文本输入一样。通常,通过输入一个特殊的meta键,接着输入一个字母键即可创建命令。例如:
  • Ctrl+C:中止或退出。
  • Ctrl+K:删除行到缓冲区。
  • Ctrl+Y:从缓冲区内容中复制或粘贴。
  • Ctrl+S:保存已编辑文本等。

2.3程序开发

2.3.1程序开发步骤

(1)创建源文件;
全局变量、局部变量、静态变量、自动变量和寄存器变量。
图2.3
Unix/Linux系统编程-学习笔记-第二章
第2章 编程背景

(2)用gcc把源文件转换成二进制可执行文件。

gcc t1.c t2.c

(3)gcc的步骤:

  1. 将C源文件转换为汇编代码文件,即将.c文件转为.s文件。
  2. 将汇编代码转换为目标代码,即将.s文件转为.o文件。
    每个.o文件包含:
  • 一个文件头
  • 一个代码段
  • 一个数据段
  • 一个BSS段
  • 指针、数据和重定位信息
  • 符号表
  1. 链接。
  • 将.o文件的所有代码段组合成单一代码段。
  • 将所有数据段组合成单一数据段。
  • 将所有BSS段组合成单一bss段。
  • 使用.o文件的重定位信息调整组合指针、数据和bss段的偏移量。
  • 用符号表来解析各个.o文件之间的交叉引用。
    图2.4
    Unix/Linux系统编程-学习笔记-第二章
第2章 编程背景

2.3.2 静态与动态链接

这是创建二进制可执行文件的两种方式。

  1. 静态库的特点:
    链接器将所有必要的库函数代码和数据纳入a.out文件中。这使得a.out文件完整、独立,但通常非常大。
  2. 动态链接的优点:
  • 减小每个a.out文件大小;
  • 许多执行程序共享相同库函数;
  • 修改库函数无需重新编译源文件。
    动态链接所用的库称为动态链接库(DLL)。它们在Linux中称为共享库(.so文件)。动态加载(DL)库是指按需加载的共享库,可用作插件和动态加载模块。

2.3.3 可执行文件格式

  1. 二进制可执行平面文件
  2. a.out可执行文件
  3. ELF可执行文件

2.3.4 a.out文件的内容

  1. 文件头
  2. 代码段
  3. 数据段
  4. 符号表

2.3.5 程序执行过程

  1. 读取a.out文件头,以确定所需总内存大小;
  2. sh从总大小中分配一个内存区给执行映像;
  3. 接着,sh放弃旧映像,开始执行新映像
  4. 执行从crt0.o开始,调用main()

2.3.6 程序终止

  1. 正常终止
  2. 异常终止

2.4 C语言中的函数调用

2.4.1 32位GCC中的运行时堆栈使用情况

  1. 进程执行映像
    图2.7
    Unix/Linux系统编程-学习笔记-第二章
第2章 编程背景

  2. 每个CPU都有以下寄存器或同等寄存器:

  • PC(IP):指向CPU要执行的下一条指令。
  • SP(SP):指向栈顶。
  • FP(BP):指向当前激活函数的栈帧。
  • 返回值寄存器(AX):函数返回值的寄存器。
  1. main函数由crt0.o调用。
    图2.8
    Unix/Linux系统编程-学习笔记-第二章
第2章 编程背景

  2. 每个函数入口,编译后的代码完成如下功能:

  • 将FP压栈,在堆栈上保存CPU的FP寄存器。
  • 让FP指向保存的FP#建立栈帧。
  • 向下移动SP为堆栈上的自动局部变量分配空间。
  • 编译后的代码可能继续向下移动SP,在堆栈上分配一些临时工作空间,用temps表示。

2.5 C语言程序与汇编代码的链接

2.5.2 汇编代码说明

三部分构成:

  1. 入口代码:又叫作prolog,它建立栈帧,在堆栈上分配局部变量和工作空间。
  2. 函数体代码:在AX寄存器中执行带有返回值的函数任务。
  3. 退出代码:又叫作epilog,它释放堆栈空间并返回到调用者。

2.6 链接库

2.6.1 创建静态链接库

gcc -c mysum.c
ar rcs libmylib.a mysum.o
gcc -static t.c -L. -lmylib
a.out

2.6.2 创建动态链接库

gcc -c -fPIC mysum.c
gcc -shared -o libmylib.so mysum.o
gcc t.c -L. -lmylib
export LD_LIBRARY_PATH=./
a.out

2.7 makefile

至此,我们已经可以用单个gcc命令来编译链接C语言程序的源文件了。为了方便,我们还可以使用包含所有命令的sh脚本。这些方案都有一个很大的缺点。如果只更改几个源文件,sh命令或脚本仍会编译所有的源文件,包括未修改的文件,这不但没有必要,而且浪费时间。一个更好的方法是使用Unix/Linux make工具(GNU make 2018)。make是一个程序,它按顺序读取makefile或Makefile,以自动有选择地执行编译链接。本节将介绍makefile的基础知识,举例说明它们的用法。

2.7.1 makefile格式

一个make文件由一系列目标项、依赖项和规则组成。目标项通常是要创建或更新的文件,但它也可能是make程序要引用的指令或标签。目标项依赖于一系列源文件、目标文件甚至其他目标项,具体描述键依赖项列表。规则是使用依赖项列表构建目标项所需的命令。

目标项 依赖项列表
target: file1 file2...fileN
规则
<tab> command1
<tab> command2
<tab> other command

注:当你想在markdown文档里面输出<tab>或<br>时,可以像这样,在前面加上进行转义。

2.7.2 make程序

当make程序读取makefile时,它通过比较依赖项列表中源文件的时间戳来确定要构建哪些目标项。如果任何依赖项在上次构建后有较新的时间戳,make将执行与目标项有关的规则。假设我们有一个C语言程序包含3个源文件:

  1. type.h file: // 头文件

    int mysum(int x, int y) // types, constants,etc
  2. mysum.c file // C语言中的函数
    #include <stdio.h>
    #include "type.h"
    int mysum(int x, int y)
    {
        return x+y;
    }
    
  3. t.c file:
    #include <stdio.h>
    #include "type.h"
    int main()
    {
        int sum = mysum(123,456);
        printf("sum = %d
    ", sum);
    }
    
    通常,我们会使用sh命令

    gcc -o myt main.c mysum.c

    生成一个名为myt的二进制可执行文件。下面我们将使用makefile演示C语言程序设计的编译链接。

2.7.3 makefile示例

示例2.5:makefile。
(1)创建名为mk1的makefile,包括:

myt:type.h t.c mysum.c     # target: dependency list
    gcc -o myt t.c mysum.c  # rule: line MUST begin with a TAB

在本示例中,生成的可执行文件名myt通常与目标项名称匹配。这允许make通过将目标项时间戳与依赖项列表中的时间戳进行比较,来决定稍后是否再次构建目标项。
(2)使用mk1作为makefile运行make:make通常使用默认的makefile或Makefile,即当前目录中出现的makefile。它可以通过-f标志直接使用另一个makefile,如:

make -f mk1

make将构建目标文件myt,并将命令执行显示为:

gcc -o myt t.c mysum.c

(3)再次运行make命令,将会显示消息:

make:'myt' is up to date

在这种情况下,make不会再次构建目标,因为在上次构建后没有任何文件更改。

(4)相反,如果依赖项列表中的任何文件有更改,make将再次执行rule命令。一种简单的文件修改方法是使用touch命令,修改文件的时间戳。那么,如果我们输入sh命令:

touch type.h // or touch *.h, touch *.c, etc.

make -f mk1

make将重新编译链接源文件,以生成新的myt文件。
(5)如果我们从依赖项列表中删除一些文件名,即使这些文件有更改,make也不会执行rule命令。读者可以尝试自行验证。
可以看出,mk1是一个非常简单的makefile,它与sh命令的差别不大。但是,我们可以改进makefile,使之更加灵活和通用。
示例2.6:makefile中的宏。
(1)创建一个名为mk2的makefile,包括:

CC = gcc            # define CC as gcc
CFLAGS = -Wall      # define CLAGS as flags to gcc
OBJS = t.o mysum.o  # define Object code files
INCLUDE = -Ipath    # define path as an INCLUDE directory

myt: type.h $(OBJS) # target: dependency: type.h and .o files
    $(CC) $(CFLAGS) -o t $(OBJS) $(INCLUDE)

在makefile中,宏定义的符号——$(符号)被替换为它们的值。如$(CC)被替换为gcc,$(CFLAGS)被替换为-Wall等。对于依赖项列表中的每个.o文件,make首先会将相应的.c文件编译成.o文件。但是,这只适用于.c文件。由于所有.c文件都依赖于.h文件,所以我们必须在依赖项列表中显示地包含type.h(或任何其他.h文件)。或者,我们可以定义其他目标项来指定.o文件对.h文件的依赖关系,如:

t.o: t.c type.h         # t.o depend on t.c and type.h
    gcc -c t.c
mysum.o: mysum.c type.h # mysum.o depend type.h
    gcc -c mysum.c

如果我们将上述目标项添加到makefile中,.c文件或type.h中的任何更改都将出发make重新编译.c文件。如果.c文件的数量很小,则会很有效。如果.c文件的数量很大,则会很繁琐。因此,有更好的方法将.h文件包含在依赖项列表中,稍后将进行展示。

(2)以mk2作为makefile运行make。

make -f mk2

(3)按前面一样运行生成的二进制可执行文件myt。
示例2.5和示例2.6的简单makefile足以编译链接大多数小型C语言程序。以下显示了makefile的一些附加功能。

示例2.7:按名称编译目标。

当make在makefile上运行时,通常会尝试在makefile中构建第一个目标。通过指定一个目标名称可以更改make的行为,从而make将设置特定的命名目标。以名为mk3的makefile为例,其中新功能以粗体字母突出显示。

# ---------------- mk3 file --------------------------------
CC = gcc                # define CC as gcc
CFLAGS = -Wall          # define CLAGS as flags to gcc
OBJS = t.o mysum.o      # define Object code files
INCLUDE = -Ipath        # define path as an INCLUDE directory

all: myt install        # build all listed targets: myt, install

myt: t.o mysum.o        # target: dependency list of .o files
    $(CC) $(CFLAGS) -o myt $(OBJS) $(INCLUDE)

t.o: t.c type.h         # t.o depend on t.c and type.h
    gcc -c t.c
mysum.o: mysum.c type.h # mysum.o depend mysum.c and type.h
    gcc -c mysum.c

install: myt            # depend on myt: make will build myt first
    echo install myt to /usr/local/bin
    sudo mv myt /usr/local/bin/     # install myt to /usr/local/bin/

run: install            # depend on install, which depend on myt
    echo run executable image myt
    myt || /bin/true    # no make error 10 if main() return non-zero

clean:
    rm -f *.o 2> /dev/null          # rm all *.o files
    sudo rm -f /usr/local/bin/myt   # rm myt

读者可以通过输入以下make命令测试mk3文件:

(1).make [all] -f mk3   # build all targets: myt and install
(2).make install -f mk3 # build target myt and install myt
(3).make run -f mk3     # run /usr/local/bin/myt
(4).make clean -f mk3   # remove all listed files

makefile 变量:makefile支持变量。在makefile中,%是一个与sh中的*类似的通配符变量。makefile还可以包含自动变量,这些变量在匹配规则后由make设置。自动变量规定了对目标和依赖项列表中元素的访问,从而用户不必显示指定任何文件名。自动变量对于定义一般模式规则非常有用。以下列出了make的一些自动变量。

  • $@:当前目标名
  • $<:第一个依赖项名
  • $^:所有依赖项名
  • $*:不包含扩展名的当前依赖项名
  • $?:比当前目标更新的依赖项列表
    另外,make还支持后缀规则,后缀规则并非目标,而是make程序的指令。我们通过一个例子来说明make变量和后缀规则。
    在C语言程序中,.c文件通常依赖于所有.h文件。如果任何.h文件发生更改,则必须编译所有.c文件。为了确保这一点,我们可以定义一个包含所有.h文件的依赖项列表,并在makefile中指定一个目标:
DEPS = type.h       # list ALL needed .h files
%.o: %.c $(DEPS)    # for all .o files: if its .c or .h file changed
    $(CC) -c -o $@  # compile corresponding .c file again

在上面的目标中,%.o代表所有.o文件,$@设置为当前目标名称,即当前.o文件名。这样可以避免为单个.o文件定义单独的目标。
示例2.8:使用make变量和后缀规则。

# ---------------- mk4 file --------------------------------
CC = gcc
CFLAGS = -I.
OBJS = t.o mysum.o
AS = as     # assume we have .s files in assembly also
DEPS = type.h   # list all .h files in DEPS

.s.o: # for each fname.o, assemble fname.s into fname.o
    $(AS) -o $< -o $@   # -o $@ REQUIRED for .s files

.c.o: # for each fname.o, compile fname.c into fname.o