跳到主要内容

07-10 链接器及汇编语言混合编程

07 揭开链接器的面纱(上)

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

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

目标文件的秘密

  • 各个段没有具体的起始地址,只有段大小信息
  • 各个标识符没有实际地址,只有段中的相对地址
  • 段和标识符的实际地址需要链接器具体确定

链接器的工作内容:将目标文件和库文件整合为最终的可执行程序。

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

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

默认情况下(gcc)

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

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

__libc_start_main()函数的作用

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

程序的启动过程

自定义程序入口函数

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

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

    #include <stdio.h>
    #include <stdlib.h>
    int program() { //Entry Function
    printf("Hello World!");
    exit(0);
    }
    // gcc main.c -e program -nostartfiles

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

重要链接选项

  • -nostartfiles Do not use the standard system startup fles when linking. The standard system libraries are used normally, unless ‘-nostdlib’ or ‘-nodefaultlibs’ is used.
  • -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, such as ‘-static-libgcc’ or ‘-shared-libgcc’, are ignored. The standard startup fles are used normally, unless ‘-nostartfiles’ is used. The compiler may generate calls to memcmp, memset, memcpy and memmove. These entries are usually resolved by entries in libc. These entry points should be supplied through some other mechanism when this option is specifed.
  • -nostdlib Do not use the standard system startup fles or libraries when linking. No startup fles and only the libraries you specify are passed to the linker, and options specifying linkage of the system libraries, such as ‘-static-libgcc’ or ‘-shared-libgcc’, are ignored. The compiler may generate calls to memcmp, memset, memcpy and memmove. These entries are usually resolved by entries in libc. These entry points should be supplied through some other mechanism when this option is specifed.

08 揭开链接器的面纱(中)

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

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

  • 合并各个目标文件中的段
  • 重定位各个段的起始地址
  • 重定位各个符号的最终地址

链接脚本的本质

链接脚本初探(Round 1)

SECTIONS    			//关键字,描述各个段在内存中的布局
{
.text 0x2000000: //代码段起始地址
{
*(.text) //所有目标文件中的代码段合并进入可执行程序
}
. = 0x8000000 //设置当前地址
S = .; //设置标识符S的存储地址
.data 0x3000000:
{
*(.data)
}
.bss:
{
*(.bss)
}
}

注意事项

  • 各个段的链接地址必须符合具体平台的规范
  • 链接脚本中能够直接定义标识符并指定存储地址
  • 链接脚本中能够指定源代码中标识符的存储地址

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

编程体验:初体验

gcc -o a.out  main.c linker.lds 
//linker.lds
SECTIONS
{
.text 0x08048400:
{
*(.text)
}

. = 0x01000000;

s1 = .;

. += 4;

s2 = .;

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

.bss :
{
*(.bss)
}
}
//main.c
#include <stdio.h>

int s1;
extern int s2;

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

return 0;
}

MEMORY及ENTRY命令

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

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

MEMORY命名的使用(Round 2)

MEMORY
{
RAM0(WX):ORIGIN = 0x02000000,LENGTH = 1024k
RAM1(!X):ORIGIN = 0x4000000,LENGTH = 256k
}
SECTIONS
{
.text:{ *(.text) } > RAM0
.data:{ *(.data) } > RAM1
.bss:{ *(.bss) } > RAM1;
}

MEMORY命令的属性定义

标识说明
R只读
W可读可写
X可执行
A可分配
I已初始化
L已初始化
!属性反转

ENTRY命令指定入口点(Round 3)

ENTRY(entry)	//entry为入口函数
SECTIONS
{
.text:{ *(.text) }
.data:{ *(.data) }
.bss:{ *(.bss) }
}

编程实验:指定入口函数

gcc -o a.out main.c linker.lds -nostartfiles
//linker.lds
ENTRY(program)

SECTIONS
{
.text 0x08048400:
{
*(.text)
}
}
//main.c
#include <stdio.h>
#include <stdlib.h>

int program()
{
printf("Hello World!\n");

exit(0);
}

小结

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

09 汇编语言的内嵌编程

C语言中的内嵌汇编,内嵌汇编的语法格式:

内嵌汇编示例

int main(){
int result = 0;
int input = 1;
asm volatile(
"mov %1,%0\n"
:"=r"(result) //输出变量(与汇编交互)
:"r"(input)); //输入变量(与汇编交互)
}
printf("result = %d\n",result);
printf("input = %d\n",input);
return 0;
}

编译器做了什么?

  • 将result关联到某个适合的寄存器
  • 将input关联到另一个适合的寄存器
  • 通过通用寄存器间接操作变量

常用限制符的说明

限制符说明
r通用寄存器
aeax,ax,al
bebx,bx,bl
cecx,cx,cl
dedx,dx,dl
Sesi,si
Dedi,di
q寄存器a,b,c,d
m使用合法内存代表参数
g任意寄存器,内存,立即数

编程实验:初体验

#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("input = %d\n", input);

asm volatile (
"movl %%eax, %%ecx\n"
"movl %%ebx, %%eax\n"
"movl %%ecx, %%ebx\n"
: "=a"(a), "=b"(b)
: "a"(a), "b"(b)
);

printf("a = %d\n", a);
printf("b = %d\n", b);

return 0;
}

汇编进行系统调用

问题:如何在不使用printf()的情况下打印字符串?

通过INT 80H使用内核服务

  • INT指令用于使用Linux内核服务(中断指令)
  • 80H是一个中断向量号,用于执行系统调用

A:如何指定具体的系统调用(如:sys_write)以及调用参数?

B:通过寄存器指定具体的系统调用及参数。

INT 80H使用示例一

char *s = "Hello.world\n";
int l = 12;
asm volatile(
"movl $4,%%eax\n" //指定编号为4的系统调用(sys_write)
"movl $1,%%ebx\n" //指定sys_write的输出目标,1为标准输出
"movl %0,%%ecx\n" //指定输出字符串地址
"movl %1,%%edx\n" //指定输出字符串长度
"int $0x80 \n" //执行系统调用
: //忽略输出参数
:"r"(s),"r"(l)
:"eax","ebx","ecx","edx"); //保留寄存器,不同于关联变量
)

INT 80H使用示例二

asm volatile(
"movl $1,%eax\n" //指定编号为1的系统调用(sys_exit)
"movl $42, %ebx \n" //指定sys_exit的参数,即退出码
int $0x80 \n”); //执行系统调用
);

注意事项

  • 嵌入汇编时,除汇编模板外,其余参数可以全部省略
  • 当省略的参数在中间时,对应分隔符“:”不可省略
  • 当省略保留列表时,对应分隔符":"可省略
  • 当省略可选参数时,寄存器前使用单个%作为前缀
  • 当存在可选参数时,寄存器前使用两个%作为前缀

编程实验:深入体验

#include <stdio.h>

int main() {
char* s = "Hello.world\n";
int l = 12;

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完成代码编译
  • 运行后在屏幕上打印“Hello World”
#include <stdio.h>
int main(){
printf("Hello World\n");
return 0;
}

深度分析

解决方案设计

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

打印函数设计

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

退出函数设计

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

链接脚本设计

SIZEOF_HEADERS为Linux应用程序的头部所占用的内存大小。

ENTRY(program)	//指定入口函数为program
SECTIONS
{
.text 0x08048000 + SIZEOF_HEADERS:
{ //将所有目标文件中的.text和.rodata段合并进入可执行程序的.text段中
*(.text)
*(.rodata)
}
/DISCARD/:
{ //放弃所有目标文件中的.text和.rodata之外的其他段
*(*)
}
}

最后的准备

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

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

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

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

编程实验:模拟嵌入式开发

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

void program() {
print("Hello World\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"
);
}
ENTRY(program)

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

/DISCARD/ :
{
*(*)
}
}
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)

小结

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