【读书笔记】程序员的自我修养总结(五)

版权声明:本文为博主原创文章,转载请注明出处http://blog.csdn.net/lg1259156776/。 https://blog.csdn.net/LG1259156776/article/details/49465043

【读书笔记】程序员的自我修养总结(五)


声明:引用请注明出处http://blog.csdn.net/lg1259156776/


说明:这是程序员的自我修养一书的读书总结,随着阅读的推进,逐步增加内容。


COMMON块

前面提到过强弱符号机制允许同一个符号的定义存在于多个文件中,编译器知道变量数据类型,而链接器则不知道数据类型,即变量类型对链接器是透明的,只知道一个符号的名字,并不知道类型是否一致。当定义多个类型不一致的符号时,链接器该如何处理呢?

一种情况是两个强符号,当然直接链接器报错,不允许强符号多重定义;
二种情况是一个强符号,其余是弱符号。肯定直接只选择强符号,而不管数据类型的空间那个大;
三种情况是多个弱符号,选择数据类型占据内存空间最大的那个定义。

当编译器将一个编译单元编译成目标文件时,如果编译单元包含了弱符号,那么弱符号最终占据空间大小是未知的,因为它不知道其他编译单元中该弱符号占据的空间大小,所以编译器无法为该弱符号在BSS段分配空间,因为所需空间大小未知。但链接器在链接过程中可以确定弱符号的大小,因为链接器读取所有输入目标文件以后,任何一个弱符号的最终大小都可以确定,可以在最终的输出文件的bss段为其分配空间。总体上看,未初始化的全局变量最终还是被放到BSS段的。

这里再说明一下:
未初始化全局变量在目标文件中是没有分配空间的,因为上面所讲原因,而初始化的全局变量是强符号,在目标文件中是已经分配地址了。

早期C语言程序员粗心大意,经常忘记使用extern声明变量,使得编译器会在多个目标文件中产生同一个变量的定义。为了解决这个问题,编译器和链接器干脆把未初始化的变量都当作COMMON类型处理。

静态链接库

比如经典的C语言版本Hello world,使用C语言标准库printf输出字符串,printf函数对字符串进行一系列处理后,最后会调用系统API,各个操作系统下,往终端输出字符串的API都不一样,在Linux下是一个write的系统调用,而在windows下是一个writeConsole系统API。

静态库可以看做一组目标文件的集合,即很多目标文件经过压缩打包后,形成一个文件。使用ar命令,或者windows下的lib.exe,用来创建、提取和列举lib中的内容。

一般库文件中的目标文件之间也大多是相互依赖的,所以如果靠人工将所有相互依赖的目标文件找出来进行链接,估计会死人。所以链接器代替人工来处理这件事,自动寻找所有需要的符号及所在的目标文件,并将这些目标文件从对应的库中解压出来,最终将他们连接在一起成为可执行文件。

有一点值得提出:为何静态链接库中一个目标文件只包含一个函数呢?
链接器在链接静态库的时候是以目标文件为单位的,比如引用了静态库中的printf函数,那么链接器就会把库中包含printf函数的那个目标文件链接进来,如果很多函数放入同一个目标文件中,很多可能没有用的函数都被一起链接到了输出文件中,这样会造成空间浪费,所以那些不需要的函数就不要链接到最终的输出文件中。

最小的程序

如下:

char * str = "Hello world!\n";
void print()
{
    asm("movl $13, %%edx \n\t"
        "movl %0, %%ecx \n\t"
        "movl %0, %%ebx \n\t"
        "movl %4, %%eax \n\t"
        "int $0x80      \n\t"
        :: "r"(str):"edx","ecx","ebx");
}

void exit()
{
    asm("movl %42, %%ebx \n\t"
        "movl %1, %%eax \n\t"
        "int $0x80      \n\t");
}

void nomain()
{
    print();
    exit();
}

分析源代码,程序入口为nomain()函数,然后调用print函数,打印hello world,接着调用exit函数结束进程。这里的print使用的是Linux的Write系统调用,exit使用的是EXIT系统调用,使用GCC的内嵌汇编。系统调用通过0x80中断实现,其中eax为调用号,ebx,ecx,edx等通用寄存器用来传递参数,比如WRITE调用是往一个文件句柄写入数据,其C语言原型为:

**int write(int filedesc, char * buffer, int size);**
  • WRITE调用的调用号位4,则eax=4;
  • filedesc表示写入的文件句柄,使用ebx寄存器传递,往默认终端stdout输出,句柄为0,所以ebx=0。
  • buffer表示写入缓存区地址,使用ecx寄存器传递,输出字符串,所以ecx = str。
  • size表示写入字节数,使用edx寄存器传递,字符串的长度为13个字节,所以edx=13。

同理可以分析EXIT系统调用,ebx表示进程退出码,比如main程序中的return返回给系统库,系统库将该数值传递给EXIT系统调用,这样父进程就可以接收到子进程的退出码,EXIT的系统调用调用号为1,所以eax=1。

第一步将编译为目标文件,然后使用ld将其链接为TinyHelloWorld,命令如下:

gcc -c -fno-builtin TinyHelloWorld.c
ld -static -e nomain -o TinyHelloWorld TinyHelloWorld.c

说明:
-fno-builtin gcc提供的内置函数,将一些常用的c库函数替换为编译器内置函数,达到优化功能。这里选择将该功能关闭;
-static表示静态链接,而不是使用默认的动态链接形式;
-e nomain表示程序入口函数为nomain。对应将ELF文件头中的e_entry成员赋值为nomain函数地址。

使用objdump或readlf查看,有四个段,.text,.data,.rodata,.comment。

实际上四个段都是只读的,原则上可以将其合并到一个段中,该段的属性是可执行可读,包含数据和指令。

链接脚本

其实有点类似于cmd文件,在DSP的编程开发中,一个非常重要的内容就是cmd的编写,内存的编排:

ENTRY(nomain)
SECTIONS
{
    . = 0x08048000 + SIZEOF_HEADERS;
    tinytext : {*(.text) *(.data) *(.rodata)}
    /DISCARD/ : { *(.comment)}
}

非常简单的链接脚本,第一行指定入口地址为nomain,然后是第一条为赋值语句,后两条为段转换规则;
第一条语句将当前虚拟地址设置为0x08048000 + SIZEOF_HEADERS;宏为输出文件的文件头大小,”.”表示当前虚拟地址。
tinytext将所有的段依次合并到该段;
最后一条说明将.comment段忽略掉,不保存在文件中。

gcc -c -fno-builtin TinyHelloWorld.c
ld -static -T TinyHelloWorld.lds -o TinyHelloWorld TinyHelloWorld.c

2015-10-27读书笔记 张朋艺

展开阅读全文

没有更多推荐了,返回首页