GCC全过程详解+剖析生成的.o文件

使用GCC编译一个.c文件影藏了哪些过程?



GCC四步详解

第一步:预处理(也叫预编译)
        gcc -E  hello.c  -o hello.i
         或者 cpp hello.c > hello.i     【cpp是预编译器】
         将所有#define删除,并且展开所有的宏定义
         处理所有的条件预编译指令,如#if #ifdef  #undef  #ifndef  #endif #elif
         处理#include,将包含的文件插入到此处,这是一个递归的过程
         删除所有注释   //   /* */
         添加行号和文件名标识,以便于编译时产生的错误警告能显示行号
         保留#pragma编译器指令
第二步:编译
         gcc  -S  hello.i   -o  hello.s
         将预处理完的.i文件进行一系列的词法分析、语法分析、语义分析及优
         化后生成响应的汇编代码文件,这是整个程序构建的最核心的部分,也是最复杂的部分

第三步:汇编
        gcc  -c  hello.s  -o  hello.o或者 as  hello.s -o  hello.o
        汇编是将第二步生成的汇编代码编程机器可执行的指令,每一个汇编语句几乎都对应一条机器指令


第四步:链接

         链接动态库和静态库



生成的目标文件有什么,什么是目标文件?

目标文件就是源代码经过编译后但未进行链接的那些中间文件
Linux下的 .o文件就是目标文件,目标文件和可执行文件内容和
格式几乎都一样,所以我们可以广义地将目标文件和可执行文化
看成一类型文件。他们都是按照ELF文件格式存储的


Linux下有哪些ELF类型的文件?

.o文件、可执行文件、核心转储文件(core dump)、.so文件(动态链
链接库)


可执行文件的概貌详解

File  Header.text section.data section.bss section
文件头(File Header)
描述了整个文件的文件属性,包括目标文件是否可执行、是静态链接还 是动
态链接及入口地址、目标硬件、目标操作系统等信息、段表(描述文件中各
个段的偏移位置及属性等)
代码段(.text)
存放了程序源代码编译后生成的机器指令
数据段(.data)
存放已初始化的全局静态与非静态变量和已初始化的局部静态变量
.bss段
存放未初始化的全局变量(全局静态和非静态变量)和局部静态变量
但是.bss段只是为这些变量预留位置而已,并没有内容,所以这些变量
在.bss段中也不占据空间


深入挖掘 .o文件



使用命令:


objdump  -h  xxxx.o

        打印主要段的信息

objdump  -x  xxxx.o 
            打印更多的详细信息
objdump  -s  xxx.o
            将所有段的内容以16进制方式打印出来
objdump  -d  xxx.o  或者-S
            将所有包含指令的段反汇编
objdump   -t   xxx.o
            查看所有的符号以及他们所在段
readelf  -h   xxx.o
            查看.o文件的文件头详细信息
readelf   -S   xxx.o
            显示.o文件中的所有段,即查看段表
size xxx.o
            查看.o文件中各个段所占大小
nm xxx.o 
            查看.o文件中所有的符号


使用命令gcc -c test.c编译下面这个test.c程序生成test.o文件,然后查看test.o文件结构


test.c

/* this is a test code */
/* test.c */

int printf(const char *format, ...);

int g_var2 = 10;
int g_var2;

void func(int i)
{
    printf("%d\n",i);
}

int main(void)
{
    static int static_var1 = 20;
    static int static_var2;
    
    int var3 = 1;
    int var4;
    func(static_var1 + static_var2 + var3 + var4);
    return var3;
}

然后查看生成的test.o文件的结构
objdump -h test.o

行:
    .text  :代码段(存放函数的二进制机器指令)
    .data :数据段(存已初始化的局部/全局静态变量、未初始化的全局静态变量)
    .bss  :bss段(声明未初始化变量所占大小)
    .rodata :只读数据段(存放 " " 引住的只读字符串)
    .comment :注释信息段
    .node.GUN-stack :堆栈提示段
列:
    Size:段的长度
    File Off :段的所在位置(即距离文件头的偏移位置)
段的属性:
    CONTENTS:表示该段在文件中存在
    ALLOC :表示只分配了大小,但没有存内容


关于.bss段

我们说.bss段是存放未初始化的全局变量(静态与非静态)和局部静态变量的
所以我们程序中的g_var2和stactic_var2应该都在.bss段中被预留位置,所以
.bss段的size应该是8个字节,但是结果却是4个字节,怎么回事呢?
这就是不用的编译器实现不一样的原因了,有些编译器会将未初始化的全局非静态变量放在.bss段,有些则不放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。而我的编译器是没有将g_var2(全局未初始化的非静态变量)放在任何段
下面让我们真正的查看一下g_var2
首先,我们使用  readelf -S  test.o  查看段表(主要为了查看每个段的段号)


然后我们再使用 readelf -s  test.o看一下符号表(我们定义的变量名都是符号,包括函数名)

符号表里会显示这个符号所在的位置



我们看到static_var1和g_var1所在段的段号为3(3是.data段),static_var2所在段的段号为4(4是.bss段),而g_var2却没有被放入任何一个段,只是用COM标记了一下,那这个COM表示什么意思呢?COM标记的符号被称为弱符号,一个变量名是弱符号,则这个变量的大小在编译的时候不能被确定,而在链接之后才能确定该变量的大小。test.o文件在链接之后,g_var2会被放入.bss段(当然,也只是说明g_var2所需要的空间大小,并不会存放内容),而在程序运行的时候g_var2这样的变量才会真正去占用内存空间


强制将某变量或者某函数放入某个段

__attribute__((section(".data")))  int   g_var2;   //强制将g_var2放入.data段中


各种变量所在位置总结
    全局已初始化非静态变量、局部已初始化静态变量会被放入.data段
    全局未初始化静态变量会被放入.bss段
    全图未初始化非静态变量不会被放入任何一个段,只是用COM标记一下

猜你喜欢

转载自blog.csdn.net/gt1025814447/article/details/80442673