链接器

1 揭开链接器的面纱

  • 问题
    • 源文件被编译后生成目标文件,这些目标文件如何生成最终的可执行程序?
  • 链接器的意义
    • 链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接

这里写图片描述

  • 目标文件的秘密

    • 各个段没有具体的地址,只有段大小信息
    • 各个标识符没有实际的地址,只有段中的相对地址
    • 段和标识符的实际地址需要链接器具体确定
  • 链接器工作的内容:将目标文件和库文件整合为最终的可执行程序

    • 合并各个目标文件中的段(.text,.data,.bss)
    • 确定各个段和段中标识符的最终地址(重定位)

2 gcc下程序的执行流程

问题:main()函数是第一个被调用执行的函数吗?

2.1 默认情况下(gcc)

  1.程序加载后,_start()是第一个被调用执行的函数。
  2._start()函数准备好参数后立即调用_lib_start_main()函数。
  3._lib_start_main()初始化运行环境后调用main()函数执行(main()函数在_lib_start_main()函数中执行)。

  _start()函数的入口地址就是代码段(.text)的起始地址!

_lib_start_main()函数的作用
  1.调用_libc_csu_init()函数(完成必要的初始化)。
  2.启动程序的第一个线程(主线程),main()为线程入口。
  3.注册_lib_csu_fini()函数(程序运行终止时被调用)。

程序的启动过程

这里写图片描述

2.2 自定义程序入口函数

  gcc提供-e选项用于在连接时指定入口函数。
  自定义入口函数时必须使用-nostartfiles选项进行链接

#include <stdio.h>
#include <stdlib.h>


int program(void)
{
    printf("this is main...\n");

    exit(0);
}

执行的链接命令如下:
gcc -c main.c -o main.o
gcc -e program main.o -nostartfiles -o main.out
./a.out
this is main...

  思考:
    链接选项-nostartfiles的意义是什么?
    链接器根据什么原则完成具体的工作?

这里写图片描述

3 链接脚本

  思考:连接器根据什么原则完成具体的工作?

3.1 链接脚本的概念和意义

  概念:链接脚本用于描述链接器处理目标文件和库文件的方式。
  意义:
    合并各个目标文件中的段。
    重定位各个段的起始地址。
    重定位各个符号的最终地址。

3.2 链接脚本的本质

这里写图片描述

3.3 链接脚本初探

这里写图片描述

  注意事项:
    各个段的链接地址必须符合具体平台的规范。在Linux中,进程代码段(.text)的合法起始地址为[0x08048000,0x08049000]。
    链接脚本中能够直接定义标识符并指定存储地址。
    链接脚本中能够指定源代码中标识符的存储地址。

链接脚本初体验
test.c:

#include <stdio.h>

int s1;
extern int s2;

int main(void)
{
    printf("&s1 = %p.\n", &s1);

    return 0;
}
SECTIONS
{
    .text 0x10000000:
    {
        *(.text)
    }

    . = 0x40000000;

    S = .;

    .data :
    {
        *(.data)
    }

    .bss :
    {
        *(.bss)
    }
}

注意:并不会因为改变位置指针就会影响后面段的起始地址,只有强制指定段地址才会影响后续段的地址。

gcc test.c test.lds
使用objdump -h a.out的结果如下:
a.out:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .interp       00000013  08048174  08048174  00000174  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.ABI-tag 00000020  08048188  08048188  00000188  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .note.gnu.build-id 00000024  080481a8  080481a8  000001a8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .gnu.hash     00000020  080481cc  080481cc  000001cc  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .dynsym       00000050  080481ec  080481ec  000001ec  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynstr       0000004c  0804823c  0804823c  0000023c  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .gnu.version  0000000a  08048288  08048288  00000288  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .gnu.version_r 00000020  08048294  08048294  00000294  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .rel.dyn      00000008  080482b4  080482b4  000002b4  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .rel.plt      00000018  080482bc  080482bc  000002bc  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 10 .init         00000023  080482d4  080482d4  000002d4  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 11 .plt          00000040  08048300  08048300  00000300  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 12 .text         000001a2  10000000  10000000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 13 .fini         00000014  100001a4  100001a4  000011a4  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .rodata       00000013  100001b8  100001b8  000011b8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 15 .eh_frame_hdr 0000002c  100001cc  100001cc  000011cc  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .eh_frame     000000b0  100001f8  100001f8  000011f8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 17 .init_array   00000004  10001f08  10001f08  00001f08  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 18 .fini_array   00000004  10001f0c  10001f0c  00001f0c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 19 .jcr          00000004  10001f10  10001f10  00001f10  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 20 .dynamic      000000e8  10001f14  10001f14  00001f14  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 21 .got          00000004  10001ffc  10001ffc  00001ffc  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 22 .got.plt      00000018  10002000  10002000  00002000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 23 .data         00000008  10002018  10002018  00002018  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 24 .bss          00000014  10002020  10002020  00002020  2**2
                  ALLOC
 25 .comment      0000004f  00000000  00000000  00002020  2**0
                  CONTENTS, READONLY
SECTIONS
{
    .text 0x08048400:
    {
        *(.text)
    }

    . = 0x01000000;

    s1 = .;

    . += 4;

    s2 = .; // 链接脚本中能够直接定义标识符并指定存储位置

    .data 0x0804a800:
    {
        *(.data)
    }

    .bss :
    {
        *(.bss)
    }
}

3.4 MEMORY命令

  默认情况下,链接器认为程序应该加载进入同一个存储空间。
  嵌入式系统中,如果存在多个存储空间,必须使用MEMORY进行存储区域定义。

MEMORY命令的使用

这里写图片描述

MEMORY命令的属性定义

这里写图片描述

3.5 ENTRY命定指定入口点

这里写图片描述

链接脚本指定函数入口函数

#include <stdio.h>
#include <stdlib.h>

int program()
{
    printf("D.T.Software\n");

    exit(0); // 这里如果使用return 0 将产生段错误
}

ENTRY(program)

SECTIONS
{
    .text 0x08048400:
    {
        *(.text)
    }
}

gcc -nostartfiles test.c test.lds

链接脚本小结

  链接器根据链接脚本中的描述完成具体的工作。
  链接脚本用于指定各个段的地址和标识符的地址。
  SECTIONS命令确定可执行程序的段信息。
  MEMORY命令对存储区进行重定义。
  ENTRY命令指定可执行程序的入口函数。

4 链接器、链接脚本、makefile综合示例

4.1 课程实验(模拟嵌入式软件开发)

  • 编写一个“体积受限”的可执行程序。
  • 通过makefile完成代码编译。
  • 运行后在屏幕上打印“D.T.Software”。

4.2 深度分析

这里写图片描述

4.3 解决方案设计

  • 通过内嵌汇编自定义打印函数和退出函数(INT 80H)。
  • 通过链接脚本自定义入口函数(不依赖任何库和GCC内置功能)。
  • 删除可执行程序中的无用信息(无用段信息、调试信息等)。

打印函数设计

这里写图片描述

退出函数设计

这里写图片描述

链接脚本设计

这里写图片描述

4.4 最后的准备

ld命令:GNU的链接器,将目标文件链接为可执行程序。其是GCC编译器集中的一员,重要的幕后工作者。

ld -static:-static表示ld使用静态链接的方式来产生最终程序,而不是默认的动态链接方式。

gcc -fno-builtin:-fno-builtin参数用于关闭GCC内置函数的功能。GCC提供了很多内置函数(Built-in Function),它会把一些常用的C库函数替换成编译器的内置函数,以达到优化的目的。

4.5 编程实验

program.c


void print(const char* s, int l);
void exit(int code);

void program()
{
    print("D.T.Software\n", 13);
    exit(0);
}

void print(const char* s, int l)
{
    asm volatile (
        "movl $4, %%eax\n"
        "movl $1, %%ebx\n"
        "movl %0, %%ecx\n"
        "movl %1, %%edx\n"
        "int $0x80     \n"
        :
        : "r"(s), "r"(l)
        : "eax", "ebx", "ecx", "edx"
    );
}

void exit(int code)
{
    asm volatile (
        "movl $1, %%eax\n"
        "movl %0, %%ebx\n"
        "int $0x80     \n"
        :
        : "r"(code)
        : "eax", "ebx"
    );
}

program.lds

ENTRY(program)

SECTIONS
{
    .text 0x08048000 + SIZEOF_HEADERS :
    {
        *(.text)
        *(.rodata)
    }

    /DISCARD/ :
    {
        *(*)
    }
}

makefile


CC := gcc
LD := ld
RM := rm -fr

TARGET := program.out
SRC := $(TARGET:.out=.c)
OBJ := $(TARGET:.out=.o)
LDS := $(TARGET:.out=.lds)

.PHONY : rebuild clean all

$(TARGET) : $(OBJ) $(LDS)
    $(LD) -static -T $(LDS) -o $@ $<
    @echo "Target File ==> $@"

$(OBJ) : $(SRC)
    $(CC) -fno-builtin -o $@ -c $^

rebuild : clean all

all : $(TARGET)

clean :
    $(RM) $(TARGET) $(OBJ)

4.6 小结

  • 对于资源受限的嵌入式设备,需要考虑可执行程序的大小。
  • 通过内嵌汇编直接使用系统服务能够避开相关库的使用。
  • 可以通过如下方法控制可执行程序的体积大小。
    • 最小化库的使用(必要情况下考虑自己实现相关函数)。
    • 自定义链接脚本,删除无用段信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值