Skip to content

Commit

Permalink
GitBook: [master] 117 pages modified
Browse files Browse the repository at this point in the history
  • Loading branch information
Hansimov authored and gitbook-bot committed Oct 4, 2020
1 parent c629000 commit 22f761f
Show file tree
Hide file tree
Showing 10 changed files with 62 additions and 19 deletions.
8 changes: 4 additions & 4 deletions part2/07/7.1-bian-yi-qi-qu-dong-cheng-xu.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

{% tabs %}
{% tab title="code/link/main.c" %}
```text
```c
int sum(int *a, int n);

int array[2] = {1, 2};
Expand All @@ -20,7 +20,7 @@ int main()
{% tabs %}
{% tab title="code/link/sum.c" %}
```text
```c
int sum(int *a, int n)
{
int i, s = 0;
Expand All @@ -38,13 +38,13 @@ int sum(int *a, int n)
大多数编译系统提供编译器驱动程序(compiler driver),它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。比如,要用 GNU 编译系统构造示例程序,我们就要通过在 shell 中输入下列命令来调用 GCC 驱动程序:

```text
```c
linux> gcc -Og -o prog main.c sum.c
```

图 7-2 概括了驱动程序在将示例程序从 ASCII 码源文件翻译成可执行目标文件时的行为。(如果你想看看这些步骤,用 **-v** 选项来运行 GCC。)驱动程序首先运行 ✦C 预处理器(cpp)✦,它将 C 的源程序 main.c 翻译成一个 ASCII 码的中间文件 main.i:

```text
```c
cpp [other arguments] main.c /tmp/main.i
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,39 @@ Linux 系统为动态链接器提供了一个简单的接口,允许应用程

```c
#include <dlfcn.h>

void *dlopen(const char *filename, int flag);

// 返回:若成功则为指向句柄的指针,若出错则为 NULL。
```
dlopen 函数加载和链接共享库 filenameo 用已用带 RTLD\_GLOBAL 选项打开了的库解析 filename 中的外部符号。如果当前可执行文件是带 - rdynamic 选项编译的,那么对符号解析而言,它的全局符号也是可用的。flag 参数必须要么包括 RTLD\_NOW,该标志告诉链接器立即解析对外部符号的引用,要么包括 RTLD\_LAZY 标志,该标志指示链接器推迟符号解析直到执行来自库中的代码。这两个值中的任意一个都可以和 RTLD\_GLOBAL 标志取或。
```c
#include <dlfcn.h>
void *dlsym(void *handle, char *symbol);
// 返回:若成功则为指向符号的指针,若出错则为NULL。
// 返回:若成功则为指向符号的指针,若出错则为 NULL。
```

dlsym 函数的输入是一个指向前面已经打开了的共享库的句柄和一个 symbol 名字,如果该符号存在,就返回符号的地址,否则返回 NULL。

```c
#include <dlfcn.h>

int dlclose (void *handle);

// 返回:若成功则为0,若出错则为-1.
```
如果没有其他共享库还在使用这个共享库,dlclose函数就卸载该共享库。
```c
include <dlfcn.h>
const char *dlerror(void);
// 返回:如果前面对 dlopen、dlsym 或 dlclose 的调用失败,
// 则为错误消息,如果前面的调用成功,则为 NULL。
```
Expand All @@ -46,7 +54,7 @@ dlerror 函数返回一个字符串,它描述的是调用 dlopen、dlsym 或

图 7-17 展示了如何利用这个接口动态链接我们的 libvector.so 共享库,然后调用它的 addvec 例程。要编译这个程序,我们将以下面的方式调用 GCC:

```bash
```c
linux> gcc -rdynamic -o prog2r dll.c -ldl
```

Expand Down
2 changes: 1 addition & 1 deletion part2/07/7.13-ku-da-zhuang-ji-zhi.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ linux> gcc -I. -o intc int.c mymalloc.o

运行这个程序会得到如下的追踪信息:

```text
```c
linux> ./intc
malloc(32)=0x9ee010
free(0x9ee010)
Expand Down
10 changes: 6 additions & 4 deletions part2/07/7.5-fu-hao-he-fu-hao-biao.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

有趣的是,定义为带有 C static 属性的本地过程变量是不在栈中管理的。相反,编译器在 .data 或 .bss 中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号。 比如,假设在同一模块中的两个函数各自定义了一个静态局部变量 x:

```text
```c
int f()
{
static int x = 0;
Expand All @@ -36,7 +36,7 @@ C 程序员使用 static 属性隐藏模块内部的变量和函数声明,就

{% tabs %}
{% tab title="code/link/elfstructs.c" %}
```text
```c
typedef struct {
int name; /* String table offset */
char type:4, /* Function or data (4 bits) */
Expand Down Expand Up @@ -90,7 +90,7 @@ GNU READELF 程序是一个査看目标文件内容的很方便的工具。比
| temp | | | | |

{% code title="code/link/m.c" %}
```text
```c
void swap();

int buf[2] = {1, 2};
Expand All @@ -104,7 +104,7 @@ int main()
{% endcode %}
{% code title="code/link/swap.c" %}
```text
```c
extern int buf[];
int *bufp0 = &buf[0];
Expand All @@ -124,7 +124,9 @@ void swap()

> 图 7-5 练习题 7.1 的示例程序
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
这道练习题的目的是帮助你理解链接器符号和 C 变量及函数之间的关系。注意 C 的局部变量 temp 没有符号表条目。

Expand Down
4 changes: 4 additions & 0 deletions part2/07/7.7-zhong-ding-wei.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,9 @@ A. 第 5 行中对 sum 的重定位引用的十六进制地址是多少?

B. 第 5 行中对 sum 的重定位引用的十六进制值是多少?
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
这道题涉及的是图 7-12a 中的反汇编列表。目的是让你练习阅读反汇编列表,并检查你对 PC 相对寻址的理解。

Expand Down Expand Up @@ -250,7 +252,9 @@ r.addend = -4

现在假设链接器将 m.o 中的 .text 重定位到地址 0x4004d0,将 swap 重定位到地址 0x4004e8。那么 callq 指令中对 swap 的重定位引用的值是什么?
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
这道题是测试你对链接器重定位PC相对引用的理解的。给定

Expand Down
2 changes: 1 addition & 1 deletion part2/07/7.9-jia-zai-ke-zhi-hang-mu-biao-wen-jian.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

要运行可执行目标文件 prog,我们可以在 Linux shell 的命令行中输入它的名字:

```
```c
linux> ./prog
```

Expand Down
22 changes: 15 additions & 7 deletions part2/07/untitled-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,6 @@ int p2()
}
```



B.

\(a\) REF\(main.1\) → DEF\(\_\_\_\_.\_\_\)
Expand Down Expand Up @@ -262,7 +260,9 @@ int p2()
}
```
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
这是一个简单的练习,检査你对 Unix 链接器解析在一个以上模块中有定义的全局符号时所使用规则的理解。理解这些规则可以帮助你避免一些讨厌的编程错误。

Expand Down Expand Up @@ -292,19 +292,21 @@ C. 链接器选择定义在模块 2 中的强符号,而不是定义在模块 1

另一种方法是将所有的标准 C 函数都放在一个单独的可重定位目标模块中(比如说 libc.o 中)应用程序员可以把这个模块链接到他们的可执行文件中:

**`linux> gcc main.c /usr/lib/libc.o`**
```c
linux> gcc main.c /usr/lib/libc.o
```

这种方法的优点是它将编译器的实现与标准函数的实现分离开来,并且仍然对程序员保持适度的便利。然而,一个很大的缺点是系统中每个可执行文件现在都包含着亠份标准:函数. 集合的完全副本,这对磁盘空间是很大的浪费。(在一个典型的系统上,libc.a 大约是 5MB,而 libm.a 大约是 2MB。)更糟的是,每个正在运行的程序都将它自己的这些函数的副本放在内存中,这是对内存的极度浪费。另一个大的缺点是,对任何标准函数的任何改变,无论多么小的改变,都要求库的开发人员重新编译整个源文件,这是一个非常耗时的操作,使得标准函数的开发和维护变得很复杂。

我们可以通过为每个标准函数创建一个独立的可車定位文件,把它们存放在一个为大家都知道的目录中来解决其中的一些问题。然而,这种方法要求应用程序员显式地链接合适的目标模块到它们的可执行文件中,这是一个容易出错而旦耗时的过程:

```bash
```c
linux> gcc main.c /usr/lib/printf.o /usr/lib/scanf.o ...
```

静态库概念被提出来,以解决这些不同方法的缺点。相关的函数可以被编译为独立的目标模块,然后封装成一个单独的静态库文件。然后,应用程序可以通过在命令行上指定单独的文件名字来使用这些在库中定义的函数。比如,使用 C 标准库和数学库中函数的程序可以用形式如下的命令行来编译和链接:

```bash
```c
linux> gcc main.c /usr/lib/libm.a /usr/lib/libc.a
```

Expand Down Expand Up @@ -425,11 +427,15 @@ linux> gcc static ./libvector.a main2.c

关于库的一般准则是将它们放在命令行的结尾。如果各个库的成员是相互独立的(也就是说没有成员引用另一个成员定义的符号),那么这些库就可以以任何顺序放置在命令行的结尾处。另一方面,如果库不是相互独立的,那么必须对它们排序,使得对于每个被存档文件的成员外部引用的符号 S,在命令行中至少有一个 S 的定义是在对 S 的引用之后的。比如,假设 foo.c 调用 libx.a 和 libz.a 中的函数,而这两个库又调用 liby.a 中的函数。那么,在命令行中 libx.a 和 libz.a 必须处在 liby.a 之前:

**`linux>gcc foo.c libx.a libz.a liby.a`**
```c
linux>gcc foo.c libx.a libz.a liby.a
```

如果需要满足依赖需求,可以在命令行上重复库。比如,假设 foo.c 调用 libx.a 中的函数,该库又调用 liby.a 中的函数,而 liby.a 又调用 libx.a 中的函数。那么 libx.a 必须在命令行上重复出现:

**`linux> gcc foo.c libx.a liby.a libx.a`**
```c
linux> gcc foo.c libx.a liby.a libx.a
```

另一种方法是,我们可以将 libx.a 和 liby.a 合并成一个单独的存档文件。

Expand All @@ -445,7 +451,9 @@ B. p.o → libx.a → liby.a

C. p.o → libx.a → liby.a 且 liby.a → libx.a → p.o
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
在命令行中以错误的顺序放置静态库是造成令许多程序员迷惑的链接器错误的常见原因。然而,一旦你理解了链接器是如何使用静态库来解析引用的,它就相当简单易懂了。这个小练习检查了你对 这个概念的理解:

Expand Down
2 changes: 2 additions & 0 deletions part2/di-8-zhang-yi-chang-kong-zhi-liu/8.2-jin-cheng.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@
| AC | |
| BC | |
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
进程 A 和 B 是互相并发的,就像 B 和 C 一样,因为它们各自的执行是重叠的,也就是一个进程在另一个进程结束前开始。进程 A 和 C 不是并发的,因为它们的执行没有重叠;A 在 C 开始之前就结束了。
{% endtab %}
Expand Down
15 changes: 15 additions & 0 deletions part2/di-8-zhang-yi-chang-kong-zhi-liu/8.4-jin-cheng-kong-zhi.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ Unix 提供了大量从 C 程序中操作进程的系统调用。这一节将描
```c
#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

// 返回:调用者或其父进程的 PID。
```
Expand All @@ -29,7 +31,9 @@ getpid 和 getppid 函数返回一个类型为 pid\_t 的整数值,在 Linux
```c
#include <stdlib.h>
void exit(int status);
// 该函数不返回。
```

Expand All @@ -40,6 +44,7 @@ exit 函数以 status 退出状态来终止进程(另一种设置退出状态
```c
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

// 返回:子进程返回 0,父进程返回子进程的 PID,如果出错,则为 -1。
Expand Down Expand Up @@ -124,7 +129,9 @@ A. 子进程的输出是什么?

B. 父进程的输出是什么?
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
在图 8-15 的示例程序中,父子进程执行无关的指令集合。然而,在这个程序中,父子进程执行的指令集合是相关的,这是有可能的,因为父子进程有相同的代码段。这会是一个概念上的障碍,所以请确认你理解了本题的答案。图 8-47 给出了进程图。

Expand Down Expand Up @@ -250,7 +257,9 @@ int main()
```
{% endcode %}
{% endtab %}
{% endtabs %}
{% tabs %}
{% tab title="答案" %}
我们知道序列 acbc、abcc 和 bacc 是可能的,因为它们对应有进程图的拓扑排序(图 8-48)。而像 bcac 和 cbca 这样的序列不对应有任何拓扑排序,因此它们是不可行的。
Expand Down Expand Up @@ -397,7 +406,9 @@ A. 这个程序会产生多少输出行?

B. 这些输出行的一种可能的顺序是什么?
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
A. 只简单地计算进程图(图 8-49)中 printf 顶点的个数就能确定输出行数。在这里,有 6 个这样的顶点,因此程序会打印 6 行输出。

Expand Down Expand Up @@ -445,7 +456,9 @@ snooze 函数和 sleep 函数的行为完全一样,除了它会打印出一条
Slept for 4 of 5 secs.
```
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
{% code title="code/ecf/snooze.c" %}
```c
Expand Down Expand Up @@ -556,7 +569,9 @@ Environment variables:
envp[27]: HOME=/usr0/droh
```
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
{% code title="code/ecf/myecho.c" %}
```c
Expand Down
4 changes: 4 additions & 0 deletions part2/di-8-zhang-yi-chang-kong-zhi-liu/8.5-xin-hao.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,9 @@ Slept for 3 of 5 secs.
linux>
```
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="答案" %}
只要休眠进程收到一个未被忽略的信号,sleep 函数就会提前返回。但是,因为收到一个 SIGINT 信号的默认行为就是终止进程(图 8-26),我们必须设置一个 SIGINT 处理程序来允许 sleep 函数返回。处理程序简单地捕获 SIGNAL,并将控制返回给 sleep 函数,该函数会立即返回。

Expand Down Expand Up @@ -663,7 +665,9 @@ int main()
```
{% endcode %}
{% endtab %}
{% endtabs %}
{% tabs %}
{% tab title="答案" %}
这个程序打印字符串 “213”,这是卡内基—梅隆大学 CS:APP 课程的缩写名。父进程开始时打印 “2”,然后创建子进程,子进程会陷入一个无限循环。然后父进程向子进程发送一个信号,并等待它终止。子进程捕获这个信号(中断这个无限循环),对计数器值(从初始值 2)减一,打印 “1”,然后终止。在父进程回收子进程之后,它对计数器值(从初始值 2)加一,打印 “3”,并且终止。
{% endtab %}
Expand Down

0 comments on commit 22f761f

Please sign in to comment.