7、链接器

xiaoxiao2021-02-28  7

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

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

文标文件的秘密:

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

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

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

链接器的工作内容:

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

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

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

nm test.out08049f28 d _DYNAMIC08049ff4 d _GLOBAL_OFFSET_TABLE_0804852c R _IO_stdin_used         w _Jv_RegisterClasses08049f18 d __CTOR_END__08049f14 d __CTOR_LIST__08049f20 D __DTOR_END__08049f1c d __DTOR_LIST__08048594 r __FRAME_END__08049f24 d __JCR_END__08049f24 d __JCR_LIST__0804a018 A __bss_start0804a00c D __data_start080484e0 t __do_global_ctors_aux08048340 t __do_global_dtors_aux0804a010 D __dso_handle         w __gmon_start__080484da T __i686.get_pc_thunk.bx08049f14 d __init_array_end08049f14 d __init_array_start08048470 T __libc_csu_fini08048480 T __libc_csu_init         U __libc_start_main@@GLIBC_2.00804a018 A _edata0804a028 A _end0804850c T _fini08048528 R _fp_hw08048294 T _init08048310 T _start0804a018 b completed.70650804a00c W data_start0804a01c b dtor_idx.7067080483a0 t frame_dummy0804845c T func0804a020 B g_global0804a024 B g_pointer0804a014 D g_test080483c4 T main

         U printf@@GLIBC_2.0

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

默认情况下(linux  gcc环境)

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

2、_start()函数准备好参数后立即调用 _libc_start_main()函数。初始化运行环境

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

_start()函数的入口地址就是可执行程序代码段(.text)的起始地址。

 gcc -c program.c -o program.o gcc program.o -o program.out ./program.out

objdump -S program.out >result.txt  //反编译结果放到result.out

_start()函数将main()函数入口地址调用_libc_start_main()函数。

objdump -h program.out 得到代码段的入口地址08048340,而08048340 <_start>:

当一个程序加载成功后,pc指针指向_start入口地址,执行_start中的指令。做参数准备工作。将参数压到栈中。

_libc_start_main()函数的作用:

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

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

3、注册_libc_csu_fini()函数(程序运行终止时被调用)。 清理函数:清理工作。

一个可执行程序运行后得到一个进程,进程中有个线程,就是主线程,主线程入口就是main()函数。

程序的启动过程:图

_start()函数初始化的三个参数:初始化函数的入口地址_libc_csu_init(),main函数的入口地址,清理函数的入口地址。将三个函数传入_libc_start_main()内部。

自定义程序入口函数:

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

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

 gcc -c program.c -o program.o gcc -e program -nostartfiles program.o -o program.out nm program.out08049f54 d _DYNAMIC08049ff4 d _GLOBAL_OFFSET_TABLE_0804a008 A __bss_start0804a008 A _edata0804a008 A _end         U exit@@GLIBC_2.00804823c T program

         U puts@@GLIBC_2.0

又:

objdump -h program.out Sections:   9 .text         0000001e  0804823c  0804823c  0000023c  2**2                   CONTENTS, ALLOC, LOAD, READONLY, CODE

 反汇编看到代码段的起始地址0804823c(LMA加载地址,pc执行的将要执行的第一条指令,=虚存) 与上边0804823c T program函数的入口地址是一样的,说明将要执行的就是program.out

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

-nostartfiles:链接时不使用系统中的标准启动文件。使用自定义函数而不使用_start函数

_start函数是从系统所提供的启动文件中来的。

8、链接器中

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

链接脚本的概念和意义:

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

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

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

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

链接脚本的本质:

main.o libc.a func.o -->链接器(链接脚本)->可执行程序

链接脚本初探(Round 1)

SECTIONS //关键字,描述各个段在内存中的布局 .本质:命令0

{

.text 0x20000000: //代码段的起始地址。 重定位

{  *(.text) }  //所有目标文件中的代码段合并进去可执行程序,*表示每个文件中的代码段

. =0x80000000; //设置当前地址 . 表示当前位置指针

S = .; //设置标识符S的存储地址。 S是标识符。不是让S保存地址值,而是重定位S,让S位于当前位置指针指代的地址处。重定位S。与c语言变量的本质:一段内存的别名遥相呼应。S代表一段内存的别名

.data 0x3000000:  //重定位(应该是数据段).data代码段,将.data代码段重定位到0x30000000处,也就是.data代码段的起始地址为0x300000

{ *(.data) } //将所有文件的.data代码段合并在一起,然后进入可执行程序中

.bss :     //链接器在链接脚本中发现显式指定代码段地址或者标识符地址,就链接脚本中的地址重定位,如果没有显式给出,链接器自己确认bss这个段放到哪个地址处。

{ *(.bss) }  //合并bss段

}

注意事项:

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

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

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

在Linux中,进程代码段(.text)的合法起始地址为[0x08048000, 0x08049000]

 gcc -o test-lds.out test.c test.lds./test-lds.out段错误

原因:链接脚本中的地址不合法

 22 .data         00000008  04000000  04000000  00003000  2**2  不合法

12 .text            0000017c  03000000  03000000  00001000  2**4  不合法

改成:

SECTIONS{ .text 0x08048400: { *(.text) } .data 0x0804a800: { *(.data) } .bss : { *(.bss) }

}

就可以了。

然后改:

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

}

gcc -o test-lds.out test.c test.lds ./test-lds.out&s1= 0x1000000 gcc -o test-lds.out test.c test.lds ./test-lds.out&s1= 0x1000000

&s2= 0x1000004

默认情况下:MMU来管理内存

链接器认为程序应该加载进入同一个存储空间。

嵌入式系统中:如果没有MMU

如果存在多个存储空间,必须使用MEMORY进行存储区域定义。

另一个脚本:

MEMORY命令的使用(Round 2)

MEMORY     //命令,对存储空间进行重定义

{

    RAM0(WX(可读可写可执行)) : ORIGIN=0X02000000,LENGTH=1024k  // 起始地址 ,长度

    RAM1(!X(不可执行)) : ORIGIN=0X04000000,LENGTH=256k

}

SECTIONS

{

    .text :{ *(.text)} >RAM0  //将代码段放入RAM0处

    .data : { *(.data) }>RAM1

    .bss :{ *(.bss)} >RAM1

}

例子:图

上边的WX,!X表示的是属性。

MEMORY命令的属性定义:

标识        说明

R            只读

W         可读可写

X            可执行

A            可分配

I            已初始化

L            已初始化

!           属性反转

ENTRY命令指定入口点(Round 3) //指定应用程序入口点

ENTRY(entry)  //以entry函数为整个函数的入口点

SECTIONS  //根据目标平台规范决定下面段的地址

{

    .text :{ *( .text) }

    .data : {*( .data) }

    .bss : { *( .bss) }

}

test-lds:

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

命令:

 gcc -o test.o -c test.c

gcc -nostartfiles test.o -o test-lds.out test.lds

./test-lds.out

objdump -h test-lds.out

 9 .text         0000001e  08048400  08048400  00000400  2**2

命令: nm test-lds.out08049f54 d _DYNAMIC08049ff4 d _GLOBAL_OFFSET_TABLE_0804a008 A __bss_start0804a008 A _edata0804a008 A _end         U _start         U exit@@GLIBC_2.008048400 T program

         U puts@@GLIBC_2.0

program的地址是08048400,换句话说,program在链接之后就成为了代码段中的第一个函数,也就是说代码段的起始地址就是program函数的入口地址,当应用程序被加载成功执行的时候就从代码段的第一条指令开始执行,换句话说就从program函数的第一条指令开始执行,所以program函数就成了入口函数了。

链接器为什么使用start函数作为应用程序的入口函数?

默认情况下,链接脚本指定了以什么方式进行链接。

链接器真身  ld

ld --verbose > default.lds

打开default.lds 有:

GNU ld (GNU Binutils for Ubuntu) 2.20.51-system.20100908  Supported emulations:   elf_i386   i386linux   elf_x86_64   elf_l1omusing 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/i686-linux-gnu/lib32"); SEARCH_DIR("=/usr/local/lib32"); SEARCH_DIR("=/lib32"); SEARCH_DIR("=/usr/lib32"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib");SECTIONS{

etc....

小结:

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

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

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

MEMORY命令对存储区域进行重定义。嵌入式中用的多,一般桌面应用程序用不上

ENTRY命令指定可执行程序的入口函数。 默认链接脚本用的这个

9、汇编语言的内嵌编程

C语言中的内嵌汇编:

内嵌汇编的语法格式:

asm volatile(可选,禁止编译器对汇编代码进行优化)

(“汇编指令” //汇编指令,汇编指令间用‘\n’分隔 (下边是汇编指令的可选参数)

:"=限制符"(输出参数)

:"限制符" (输入参数)--->可选参数,关联C语言和汇编语言

:保留列表

);

内嵌汇编示例代码:

int main()

{ int result=0;

int input=1;

asm volatile(

"movl %1,%0\n" //第一个参数放到第二个

: "=r"(result)   //输出变量(与汇编交互)

:"r"(input));  //输入变量(与汇编交互)

printf("result=%d\n",result);

printf("input=%d\n",input);

return 0;  }

编译器做了什么?

将result关联到某个适合的寄存器。

将input关联到另一个适合的寄存器

通过通用寄存器间接操作变量。

asm volatile(

"movl %1(占位符),%0\n" //第一个参数放到第二个

: "=r"(result)   //等号意味着后边参数是输出参数,限制符=>movl input,reult;(错)汇编语言不支持内存到内存的直接操作

:"r"(input));  // "r" 用于指示编译器自动将通用寄存器关联到变量,限制符:输入参数

变量的本质是内存的别名,只能间接操作通过寄存器赋值。把变量关联到寄存器。

常用寄存器的说明:

限制符   说明

r            通用寄存器

a            eax(整个寄存器),ax(低16位),al(低8位)

b            ebx,bx,bl

c            ecx,cx,cl

d            edx,dx,dl

S            esi,si

D            edi,di

q            寄存器a,b,c,d

m            使用合法内存代表参数

g            任意寄存器,内存,立即数

例:

0x1c (%esp)  内存

#include <stdio.h>int main(){  int result=0;  int input=1;  int a=1;  int b=2;  asm volatile(  "movl %1,%0\n"  : "=r"(result)  : "r"(input)  );  printf("result=%d\n",result);  printf("intput=%d\n",input);  asm volatile(  "movl %

转载请注明原文地址: https://www.6miu.com/read-2100376.html

最新回复(0)