问题:源文件被编译成目标文件,这些目标文件如何生成最终的可执行程序?
链接器的意义:链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接。
文标文件的秘密:
各个段没有具体的起始地址,只有段大小信息。
各个标识符没有实际地址,只有段中的相对地址。
段和标识符的实际地址需要链接器具体确定。
链接器的工作内容:
将目标文件和库文件整合为最终的可执行程序。
合并各个目标文件中的段(.text,.data,.bss)。合并各个段
确定各个段和段中标识符的最终地址(重定位)。
nm test.out
08049f28 d _DYNAMIC
08049ff4 d _GLOBAL_OFFSET_TABLE_
0804852c R _IO_stdin_used
w _Jv_RegisterClasses
08049f18 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_start
0804a00c D __data_start
080484e0 t __do_global_ctors_aux
08048340 t __do_global_dtors_aux
0804a010 D __dso_handle
w __gmon_start__
080484da T __i686.get_pc_thunk.bx
08049f14 d __init_array_end
08049f14 d __init_array_start
08048470 T __libc_csu_fini
08048480 T __libc_csu_init
U __libc_start_main@@GLIBC_2.0
0804a018 A _edata
0804a028 A _end
0804850c T _fini
08048528 R _fp_hw
08048294 T _init
08048310 T _start
0804a018 b completed.7065
0804a00c W data_start
0804a01c b dtor_idx.7067
080483a0 t frame_dummy
0804845c T func
0804a020 B g_global
0804a024 B g_pointer
0804a014 D g_test
080483c4 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.out
08049f54 d _DYNAMIC
08049ff4 d _GLOBAL_OFFSET_TABLE_
0804a008 A __bss_start
0804a008 A _edata
0804a008 A _end
U exit@@GLIBC_2.0
0804823c T program
U puts@@GLIBC_2.0
又:
objdump -h program.outSections:
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.out
08049f54 d _DYNAMIC
08049ff4 d _GLOBAL_OFFSET_TABLE_
0804a008 A __bss_start
0804a008 A _edata
0804a008 A _end
U _start
U exit@@GLIBC_2.0
08048400 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_l1om
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/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 %%eax,%%ecx\n"
"movl %%ebx,%%eax\n"
"movl %%ecx,%%ebx\n"
:"=a"(a),"=b"(b) //关联 a寄存器和a关联
:"a"(a),"b"(b)
);
printf("a=%d\n",a);
printf("b=%d\n",b);
return 0;
}
问题:如何在不使用printf来打印字符串?
通过INT 80H使用内核服务
INT指令用于使用linux内核服务(中断指令)。
80H是一个中断向量号,用于执行系统调用。
如何指定具体的系统调用(如:sys_write)以及调用参数?
通过寄存器指定指定具体的系统调用及参数.
INT 80H使用示例一:
char* s="sdfjl";
int l=5;
asm volatile(
"movl $4, %%eax\n" //指定编号为4的系统调用(sys_write)下边为3个参数
"movl $1, %%ebx\n" // 指定sys_write的输出目标,1为标准输出
"movl %0, %%ecx\n" //指定输出字符串地址 //可选参数?
"movl %1, %%edx\n" //指定输出字符串长度
"int $0x80 \n" //执行系统调用
: //输出参数为空
: "r"(s), "r"(l) //需要的是让通用寄存器和s相关联, 需要的时候... 可选参数?与上边的%0 %1搭配
: "eax", "ebx" ,"ecx", "edx" ); //保留寄存器,不用于关联变量,指令中使用了这些寄存器
INT 80H 使用示例二
asm volatile(
"movl $1, %eax\n" //指定编号为1的系统调用(sys_exit) 不用可选参数
"movl $42, %ebx\n" //指定 sys_exit的参数,即退出码 不是可选参数
"int $0x80 \n" //执行系统调用
没有可选参数,用一个%,有可选参数,两个%. 一个%为占位符
注意事项:
嵌入汇编时,除汇编模板外,其余参数可以全部省略。
当省略的参数在中间时,对应分隔符“:”不可省略。
当省略保留列表时,对应分隔符“:”可省略。
当省略可选参数时,寄存器前使用单个%作为前缀。
当存在可选参数时,寄存器前使用两个%作为前缀。
打印进程退出码:echo $?
#include <stdio.h>
int main()
{
char* s="software\n";
int l=9;
printf("main begin\n");
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"
);
asm volatile(
"movl $1, %eax\n"
"movl $42,%ebx\n"
"int $0x80 \n"
);
printf("main end\n");
return 0;
}
小结:
c程序中支持直接嵌入汇编语言进行编程。
通过寄存器到变量的关联完成汇编到c语言的交互。
内嵌汇编代码时,通过占位符指定交互的变量。
限制符指示编译器将适合的寄存器关联到变量。
通过内嵌汇编能够直接使用系统服务。
保留列表告诉编译器分配寄存器关联变量时不要使用列表中的寄存器。这些寄存器是汇编代码用到的。
10、链接器
课程实验(模拟嵌入式开发)
编写一个“体积受限”的可执行程序。
通过makefile完成代码编译。
运行后再屏幕上打印“D.T.SOFTware”
#include <stdio.h>
int main()
{ printf("d.t.software\n");
return 0; }
注意:体积受限
深度分析:
如何做到体积最小? ->独立于libc库进行编译链接!(不用printf)->编写链接脚本自定义入口函数 ->如何不依赖libc库进行打印 ->直接使用系统调用!->如何使用系统调用?->在c语言中嵌入汇编!
解决方案设计:通过内嵌汇编自定义打印函数和退出函数(INT 80H)。
通过链接脚本自定义入口函数(不依赖任何库和GCC内置功能):体积小,其他说明的段统统不要,所以用链接脚本,不在命令行指定。
删除可执行程序中的无用信息(无用段信息,调试信息,等)
打印函数设计:
退出函数设计:
链接脚本设计:
ENTRY(program) //指定入口函数为program
SECTIONS
{
.text 0x08048000+SIZEOF_HEADERS :
{
*(.text) //将所有目标文件中的.text和.rodata段合并进入可执行程序的.text段中
*(.rodata) //只读数据段,字符串存储在只读数据区,代码段也有只读属性,将只读数据段放入代码段,同样有只读属性
//减少段
}
/DISCARD/ :
{
*(*) //放弃所有目标文件中除.text和.rodata之外的其它段
}}
最后的准备(1):
ld命令:链接器真身
GNU的链接器,将目标文件链接为可执行程序。
GCC编译器集中的一员,重要的幕后工作者。
ld-static:对于大多数嵌入式开发,交叉编译时都是静态链接的。
-static表示ld使用静态链接的方式来产生最终程序,而不是默认的动态链接方式。
最后的准备(2):
gcc -fno-builtin
-fno-builtin参数用于关闭GCC内置函数的功能。
GCC提供了很多内置函数(Built-in Function),他会把一些常用的c库函数(被)替换成编译器的内置函数,以达到优化的目地。
命令:gcc -fno-builtin -o program.o -c program.c
打印函数设计:退出函数设计:
void print(const char* s,int l);
void exit(int code);
void program()
{
print("d.t.softeare\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" //第一个参数,%0为占位参数 传到ecx
"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"
);
}
链接脚本设计:
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)
编译一个普通的.c: 与上边的比较大小 这个大小为7k,上边的为512个字节
gcc -o test.o -c pro.c
gcc -o test.out test.o
小结:对于资源受限的嵌入式设备,需要考虑可执行程序的大小。
通过内嵌汇编直接使用系统服务能够避开相关库的使用。
可以通过如下方法控制可执行程序的体积大小:最小化库的使用(必要情况下考虑自己实现相关函数),自定义链接脚本,删除无用段信息。