揭开链接器的面纱

版权声明:课程笔记内容整理于狄泰软件 https://blog.csdn.net/qq_39654127/article/details/87901535

目录

1、链接器的作用

2、程序启动过程的函数调用

3、链接脚本

4、实验

5、小结


1、链接器的作用

问题

        源文件被编译后生成目标文件,这些目标文件如何生成最终的可执行程序?

            

链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接

目标文件的秘密

    - 各个段没有具体的起始地址,只有段大小信息

    - 各个标识符没有实际地址,只有段中的相对地址

    - 段和标识符的实际地址需要链接器具体确定

链接器的工作内容

    - 将目标文件和库文件整合为最终的可执行程序

         ★ 合并各个目标文件中的段(.text,.data,.bss)

         ★ 确定各个段和段中标识符的最终地址(重定位)

实验

//func.c
#include <stdio.h>

int* g_pointer;

void func()
{
    g_pointer = (int*)"D.T.Software";

    return;
}
//test.c
#include <stdio.h>

int g_global = 0;

int g_test = 1;

extern int* g_pointer;

extern void func();

int main(int argc, char *argv[])
{
    printf("&g_global = %p\n", &g_global);

    printf("&g_test = %p\n", &g_test);

    printf("&g_pointer = %p\n", &g_pointer);

    printf("g_pointer = %p\n", g_pointer);

    printf("&func = %p\n", &func);

    printf("&main = %p\n", &main);

    func();

    return 0;
}

gcc version 5.4.0(64位)

先编译

再链接

2、程序启动过程的函数调用

问题

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

 

默认情况下(gcc)

      1. 程序加载后,_start()是第一个被调用执行的函数

      2. _start()函数准备好参数后立即调用_libc_start_main()函数

      3. _libc_start_main()初始化运行环境后调用main()函数执行

           

                    执行程序时将PC指针指向代码段的起始地址

实验

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

int main()
{
    printf("D.T.Software\n");
	
    exit(0);
}

分析result.txt文件

                                   发现代码段的起始地址就是_start()函数的入口地址

_libc_start_main()函数的作用

      1. 调用_libc_csu_init()函数(完成必要的初始化操作)

      2. 启动程序的第一个线程(主线程),main()为线程入口

      3. 注册_libc_csu_fini()函数(程序运行终止时被调用)

程序的启动过程

自定义程序入口函数

    - gcc提供 -e 选项用于在链接时指定入口函数

    - 自定义入口函数时必须使用 -nostartfiles 选项进行链接

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

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

思考

      链接选项-nostartfiles的意义是什么?

     

重要链接选项

    - -nostartfiles

         Do not use the standard system startup files when linking.

         The standard system libraries are used normally,unless -nostdlib or-nodefaultlibs is used.

         即_start函数是从系统定义好的启动文件.o文件而来,使用-nostartfiles即告诉链接器不再使用_start()函数作为入口函数

    - -nodefaultlibs

         Do not use the standard system libraries when linking.

         Only the libraries you specify are passed to the linker,and options specifying linkage of the system libraries.

    - -nostdlib

         Donot use the standard system startup files or libraries when linking.

         No startup files and only the libraries you specify are passed to the linker,

         and options specifying linkage of the system libraries.

3、链接脚本

问题

         链接器根据什么原则完成具体的工作?

链接脚本的概念和意义

    - 链接脚本用于描述链接器处理目标文件和库文件的方式

          ★ 合并各个目标文件中的段

          ★ 重定位各个段的起始地址

          ★ 重定位各个符号的最终地址

链接脚本初探(Round1)

                                                        "."表示位置指针

注意事项

    - 各个段的链接地址必须符合具体平台的规范

    - 链接脚本中能够直接定义标识符并指定存储地址

    - 链接脚本中能够指定源代码中标识符的存储地址

test.c

//test.c
#include <stdio.h>

int s1;
extern int s2;//s2是在外部定义的

int main()
{
    printf("&s1 = %p\n", &s1);
    printf("&s2 = %p\n", &s2);
    
    return 0;
}

test.lds

SECTIONS
{
    .text 0x08048400:
    {
        *(.text)
    }
    
    . = 0x01000000;
    
    s1 = .;
    
    . += 4;
    
    s2 = .;
    
    .data 0x0804a800:
    {
        *(.data)
    }
    
    .bss :
    {
        *(.bss)
    }
}

在链接脚本里定义s2

 

默认情况下:链接器认为程序应该加载进入同一个存储空间

嵌入式系统中:如果存在多个存储空间,必须使用MEMORY进行存储区域定义

MEMORY 命令的使用(Round2)


                                    定义了存储空间RAM0,RAM1,指定起始地址,长度

                         将代码段放到RAM0,数据段放到RAM1

ENTRY 命令指定入口点(Round3)

//program.c
#include <stdio.h>
#include <stdlib.h>

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

test.lds

ENTRY(program)

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

program变成代码段第一个函数,说明ENTRY直接确定入口函数

问题 

        默认情况下链接器为什么会使用_start函数作为应用程序入口函数?

答案       链接脚本。默认情况下gcc提供一个链接脚本,描述了默认情况下以何种方式链接

 

4、实验

实验(模拟嵌入式开发)

       - 编写一个“体积受限”的可执行程序

       - 通过makefile 完成代码编译

       - 运行后在屏幕上打印"D.T.Software"

深度分析

解决方案设计

       - 通过内嵌汇编自定义打印函数退出函数INT 80H

       - 通过链接脚本自定义入口函数(不依赖任何库和GCC内置功能)

       - 删除可执行程序中的无用信息(无用段信息,调试信息,等)

打印函数设计

退出函数设计

链接脚本设计

最后的准备

       - ld命令

                GNU的链接器,将目标文件链接为可执行程序

                GCC编译器集中的一员,重要的幕后工作者

       - ld -static

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

       - gcc -fno -builtin

                  -fno-builtin参数用于关闭GCC内置函数的功能

编程实验(模拟嵌入式开发) program.out

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)
	
	

由于环境限制选择Ubuntu 10.10 gcc version 4.4.5(32位)

5、小结

链接器根据链接脚本中的描述完成具体的工作

链接脚本用于指定各个段的地址标识符的地址

SECTIONS 命令确定可执行程序中的段信息

MEMORY 命令对存储区域进行重定义

ENTRY 命令指定可执行程序的入口函数

对于资源受限的嵌入式设备,需要考虑可执行程序的大小

通过内嵌汇编直接使用系统服务能够避开相关库的使用

可以通过如下方法控制可执行程序的体积大小

     - 最小化库的使用(必要情况下考虑自己实现相关函数)

     - 自定义链接脚本,删除无用段信息

 

猜你喜欢

转载自blog.csdn.net/qq_39654127/article/details/87901535