C语言的本质(五):链接详解

简介: C语言的本质(五):链接详解

1 多目标文件的链接

现在我们把例子拆成两个.c文件,stack.c实现堆栈,而main.c使用堆栈:

/* stack.c */
      char stack[512];
      int top = -1;
      void push(char c)
      {
              stack[++top] = c;
      }
      char pop(void)
      {
              return stack[top--];
      }
      int is_empty(void)
      {
              return top == -1;
      }

在这段程序中top总是指向栈顶元素,所以要初始化成-1才表示空堆栈,这两种堆栈使用习惯都很常见。

/* main.c */
      #include <stdio.h>
      int a, b = 1;
      int main(void)
      {
              push('a');
              push('b');
              push('c');
              while(!is_empty())
                      putchar(pop());
              putchar('\n');
          return 0;
      }

a和b这两个变量没有用,只是为了顺便说明链接过程才加上的。编译的步骤和以前一样,可以一步编译:

$ gcc main.c stack.c -o main

也可以分多步编译:

$ gcc -c main.c
      $ gcc -c stack.c
      $ gcc main.o stack.o -o main

如果用nm命令查看目标文件的符号表,会发现main.o中有未定义的符号push、pop、is_empty、putchar,前三个符号在stack.o中定义了,在链接时做符号解析,而putchar是libc的库函数,在可执行文件main中仍然是未定义的,要在程序运行时做动态链接。

通过readelf -a main命令可以看到:main的.bss段合并了main.o和stack.o的.bss段,其中包含了变量a和stack;main的.data段合并了main.o和stack.o的.data段,其中包含了变量b和top;main的.text段合并了main.o和stack.o的.text段,包含了各函数的指令,如图所示。

结果正如我们所预料,可执行文件main的每个段中来自main.o的变量或函数都排到后面了。

实际上链接过程是由一个链接脚本(Linker Script)控制的,链接脚本决定了给每个段分配什么地址,如何对齐,哪个段在前,哪个段在后,哪些段合并到同一个Segment。

另外链接脚本还要把一些特殊地址定义成符号,例如__bss_start代表.bss段的起始地址,_end代表.bss段的结束地址,这些符号会出现在可执行文件的符号表中,加载器可以由这些符号得知.bss段的地址范围,以便把它清零。

如果用ld做链接时没有通过-T选项指定链接脚本,则使用ld的默认链接脚本,默认链接脚本可以用ld --verbose命令查看(由于比较长,只列出一些片断):

$ ld --verbose
      ...
      using internal linker script:
      ==================================================
      /* Script for -z combreloc: combine and sort reloc sections */
      OUTPUT_FORMAT("elf32-i386", "elf32-i386",
              "elf32-i386")
      OUTPUT_ARCH(i386)
      ENTRY(_start)
      SEARCH_DIR("/usr/i486-linux-gnu/lib32");SEARCH_DIR("/usr/local/lib32");   SEARCH_DIR("/lib32");   SEARCH_DIR("/usr/lib32");   SEARCH_DIR("/usr/i486-linux-gnu/lib");   SEARCH_DIR("/usr/local/lib"); SEARCH_DIR("/lib"); SEARCH_DIR("/usr/lib");
      SECTIONS
      {
        /* Read-only sections, merged into text segment: */
        PROVIDE  (__executable_start  =  SEGMENT_START("text-segment",
      0x08048000)); . = SEGMENT_START("text-segment", 0x08048000) + SIZEOF_HEADERS;
        .interp        : { *(.interp) }
        .note.gnu.build-id : { *(.note.gnu.build-id) }
        .hash          : { *(.hash) }
        .gnu.hash      : { *(.gnu.hash) }
        .dynsym        : { *(.dynsym) }
        .dynstr        : { *(.dynstr) }
        .gnu.version   : { *(.gnu.version) }
        .gnu.version_d : { *(.gnu.version_d) }
        .gnu.version_r : { *(.gnu.version_r) }
        .rel.dyn       :
      ...
        .rel.plt       :
      ...
        .init          :
      ...
        .plt          : { *(.plt) *(.iplt) }
        .text          :
      ...
        .fini          :
      ...
        .rodata        : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
      ...
        .eh_frame      : ONLY_IF_RO { KEEP (*(.eh_frame)) }
      ...
        /* Adjust the address for the data segment.  We want to adjust up to
          the same address within the page on the next page up.  */
        . = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .)
      & (CONSTANT (MAXPAGESIZE) -1)); . = DATA_SEGMENT_ALIGN (CONSTANT(MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
      ...
        .ctors         :
      ...
        .dtors         :
      ...
        .jcr          : { KEEP (*(.jcr)) }
      ...
        .dynamic       : { *(.dynamic) }
        .got          : { *(.got) *(.igot) }
      ...
        .got.plt       : { *(.got.plt)  *(.igot.plt) }
        .data          :
      ...
        _edata = .; PROVIDE (edata = .);
        __bss_start = .;
        .bss          :
      ...
        _end = .; PROVIDE (end = .);
        . = DATA_SEGMENT_END (.);
        /* Stabs debugging sections.  */
      ...
        /* DWARF debug sections.
          Symbols in the DWARF debugging sections are relative to the
      beginning
          of the section so we begin them at 0.  */
      ...
      }
      ==================================================

ENTRY(_start)指明整个程序的入口点是_start,这并不是规定死的,修改链接脚本就可以改用其他符号做入口点。

/* Read-only sections, merged into text segment: */
        PROVIDE  (__executable_start  =  SEGMENT_START("text-segment",
      0x08048000));  .  =  SEGMENT_START("text-segment",  0x08048000)   +
      SIZEOF_HEADERS;
        .interp        : { *(.interp) }
        .note.gnu.build-id : { *(.note.gnu.build-id) }
      ...

PROVIDE (__executable_start = SEGMENT_START(“text-segment”, 0x08048000));语句导出一个GLOBAL的符号__executable_start,它的值是Text Segment的起始地址(默认值是0x8048000)。

再看. = SEGMENT_START(“text-segment”, 0x08048000) +SIZEOF_HEADERS;这一句。“.”表示当前链接地址,即程序加载运行时的虚拟地址,链接器每组装一个段就把当前链接地址自动加上这个段的长度,因此各段在加载时一般是紧挨着的,中间没有空隙,

只有一种情况例外:如果在链接脚本中给“.”赋值,那么链接器组装下一个段就从赋值的新地址开始,而不是和前一个段紧挨着了。

所以这条语句表示把当前链接地址改成Text Segment的起始地址加上SIZEOF_HEADERS偏移量,后面的段从这里开始组装,后面的段依次是.interp段、.note.gnu.build-id段等(其中包括我们熟悉的.plt段、.text段和.rodata段),这些段都被组装到Text Segment中。

每个段的描述格式都是“段名 : { 组成 }”,例如.plt : { *(.plt) *(.iplt) },左边表示链接生成的文件的.plt段,右边表示所有目标文件的.plt段和.iplt段,意思是链接生成的文件的.plt段由各目标文件的.plt段和.iplt段组成。

组装完Text Segment之后又给当前链接地址赋了新值,从新的虚拟地址开始组装Data Segment:

/* Adjust the address for the data segment. We want to adjust up to
          the same address within the page on the next page up.  */
        . = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .)
      & (CONSTANT (MAXPAGESIZE) -1)); . = DATA_SEGMENT_ALIGN (CONSTANT
      (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
      ...

计算Data Segment的起始地址要做一系列对齐操作,可以结合图来理解,Data Segment从Text Segment的下一个页面开始,并且不是从该页面的起始地址开始,而是有一个偏移量。

上面这两个表达式的详细计算过程我们就不深入讨论了。计算出当前地址之后,从该地址开始组装链接脚本后面列出的几个段,例如.data段、.bss段等。

组装完Data Segment之后又给当前链接地址赋了新值,从新的虚拟地址开始组装调试信息等其他Segment:

. = DATA_SEGMENT_END (.);
        /* Stabs debugging sections.  */
      ...
        /* DWARF debug sections.
          Symbols in the DWARF debugging sections are relative to the
      beginning
          of the section so we begin them at 0.  */
      ...

关于链接脚本就介绍这么多。

从现在开始我们写的很多程序都是由多个.c文件编译链接在一起的,在gdb调试时如何指定某个.c文件中的某一行代码呢?现在我们调试这个程序,在push函数和pop函数里设断点,注意gdb命令的写法。

$ gcc stack.c main.c -g -o main
      $ gdb main
      GNU gdb (GDB) 7.1-ubuntu
      Copyright (C) 2010 Free Software Foundation, Inc.
      License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
      This is free software: you are free to change and redistribute it.
      There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
      and "show warranty" for details.
      This GDB was configured as "i486-linux-gnu".
      For bug reporting instructions, please see:
      <http://www.gnu.org/software/gdb/bugs/>...
      Reading symbols from /home/akaedu/main...done.
      (gdb) l
      1   /* main.c */
      2   #include <stdio.h>
      3
      4   int a, b = 1;
      5
      6   int main(void)
      7   {
      8       push('a');
      9       push('b');
      10      push('c');
      (gdb) l stack.c:1
      1   /* stack.c */
      2   char stack[512];
      3   int top = -1;
      4
      5   void push(char c)
      6   {
      7       stack[++top] = c;
      8   }
      9
      10  char pop(void)
      (gdb) b push
      Breakpoint 1 at 0x80483f0: file stack.c, line 7.
      (gdb) b stack.c:10
      Breakpoint 2 at 0x8048411: file stack.c, line 10.
      (gdb) r
      Starting program: /home/akaedu/main
      Breakpoint 1, push (c=97 'a') at stack.c:7
      7       stack[++top] = c;
      (gdb) c
      Continuing.
      Breakpoint 1, push (c=98 'b') at stack.c:7
      7       stack[++top] = c;
      (gdb) c
      Continuing.
      Breakpoint 1, push (c=99 'c') at stack.c:7
      7       stack[++top] = c;
      (gdb) c
      Continuing.
      Breakpoint 2, pop () at stack.c:12
      12      return stack[top--];

在gdb命令中指定某个.c文件中的某一行或某个函数,可以用“文件名:行号”或“文件名:函数名”的语法。

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
目录
相关文章
|
19天前
|
存储 自然语言处理 编译器
C语言从入门到实战——编译和链接
在C语言中,编译和链接是将源代码转换为可执行文件的两个主要步骤。 编译过程包括以下步骤: 1. 预处理:将源代码中的预处理指令(如`#include`和`#define`)替换为实际的代码。 2. 编译:将预处理后的代码转换为汇编语言。 3. 汇编:将汇编语言转换为机器码指令。 链接过程包括以下步骤: 1. 目标文件生成:将每个源文件编译后生成的目标文件(`.o`或`.obj`)进行合并,生成一个总的目标文件。 2. 符号解析:查找并解析目标文件中的所有符号(例如全局变量和函数名),以确保每个符号都有一个唯一的地址。 3. 地址重定位:根据符号表中符号的地址信息,将目标文件中的所有地址引用
47 0
|
19天前
|
C语言
链接未来:深入理解链表数据结构(二.c语言实现带头双向循环链表)
链接未来:深入理解链表数据结构(二.c语言实现带头双向循环链表)
33 0
|
19天前
|
编译器 Shell Linux
C语言的本质(六):链接详解-定义和声明
C语言的本质(六):链接详解-定义和声明
59 0
|
19天前
|
存储 自然语言处理 编译器
C语言编译和链接
ANSI C(American National Standards Institute C)是指按照美国国家标准协会(ANSI)定的C语言标准。在1989年,ANSI制定了一套C语言标准,该标准通常被称为ANSI C或C89(为了区别于后续的标准C99)。ANSI C标准被国际标准化组织(ISO)接受,并以ISO/IEC 9899:1990的形式发布。所以,ANSI C和ISO C代表的是同一种标准。
TU^
|
11天前
|
自然语言处理 编译器 C语言
C语言之编译链接
C语言之编译链接
TU^
20 0
|
7天前
|
存储 自然语言处理 编译器
玩转C语言——文件操作、预处理、编译、链接
玩转C语言——文件操作、预处理、编译、链接
14 0
|
19天前
|
存储 自然语言处理 编译器
C语言:编译与链接
C语言:编译与链接
11 1
|
19天前
|
存储 自然语言处理 编译器
“ Hello world ”中的秘密之【C语言程序编译和链接】
作为C语言最经典的代码,大家都可以轻易写出。但是代码的运行过程却很少有人清楚,接下来我将介绍代码运行的奥秘。
18 0
|
19天前
|
自然语言处理 编译器 C语言
C语言程序编译和链接
在ANSI C的任何⼀种实现中,存在两个不同的环境。 第1种是翻译环境,在这个环境中源代码被转换为可执⾏的机器指令(⼆进制指令)。 第2种是执⾏环境,它⽤于实际执⾏代码。
|
19天前
|
存储 自然语言处理 编译器
C语言:编译和链接(从.c文件到输出结果的过程)
C语言:编译和链接(从.c文件到输出结果的过程)