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


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


    声明:引用请注明出处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!
    ";
    void print()
    {
        asm("movl $13, %%edx 
    	"
            "movl %0, %%ecx 
    	"
            "movl %0, %%ebx 
    	"
            "movl %4, %%eax 
    	"
            "int $0x80      
    	"
            :: "r"(str):"edx","ecx","ebx");
    }
    
    void exit()
    {
        asm("movl %42, %%ebx 
    	"
            "movl %1, %%eax 
    	"
            "int $0x80      
    	");
    }
    
    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读书笔记 张朋艺

  • 相关阅读:
    5.14事务
    5.13Mysql数据库Database
    未来打算
    浅谈P NP NPC
    1222
    1219
    Linux初等命令
    惩罚因子(penalty term)与损失函数(loss function)
    12 14
    java 泛型思考
  • 原文地址:https://www.cnblogs.com/huty/p/8518972.html
Copyright © 2020-2023  润新知