在了解一个可执行文件的生成过程中,如果你使用windows中的IDE来完成这一工作,那对于你来说好像是一件非常容易的事情,因为你只需要按下build选项就可一完成编译和链接,然后又一个可执行文件就产生了,然而在中间发生了一系列很复杂的事情你知道吗?
在这篇文章中不举出非常详细的例子来展示如何查看一个程序的各个部分,只是介绍一些可以他有利于你了解你自己写出的程序的一些强大的工具。在进入主题之前先介绍一下程序生成可执行文件的过程,相信绝大数人都有所了解。那就是预编译, 编译, 汇编, 链接。
先大致介绍一下每个部分完成了什么功能:
1、预编译:
- 删除所有#define 并展开所有的宏定义
- 处理#if #ifdef 等
- 处理#include 将文件插入到与编译指令的位置 这是一个递归的过程
- 删除所有的注释
- 添加行号和文件名标识
- 保留所有的#pragma
2、编译
- 词法分析
- 语法分析
- 语义分析
- 中间语言产生
- 目标代码产生 与优化
gcc -S main.c -o main.s 产生汇编代码文件
3、汇编
将汇编代码转变成机器可执行的指令
as main.s -o main.o 或者直接从源文件产生 gcc -c main.c -o main.o产生了目标文件
4、链接
将目标文件与一些运行时库链接起来,这也是程序构建的核心部分。
下面来介绍一下linux下可执行文件的格式,ELF包括(relocatable File)可重定位文件 例如: linux .o
windows .obj 、(Executable file)可执行文件例如: linux /bin/bash windows :.exe、 共享文件 例如linux .so windows .dll、核心转存文件 linux core dump。我们可一用 file xxx.so 来显示相应文件格式。
接下来深入探讨一下额目标文件的格式。在查看.o 或 .obj文件的时候可一用binutils中工具 objdump查看文件内部结构。目标文件是分段(segment)存储的有利于程序运行效率的提高。分为.code或.text(代码段)、.data(数据段)、 .bss(赋值数据)
我们可以写一段程序
int printf(const char* format, ... );
int global_init_var = 86;
int global_uninit_var;
void func1(int i)
{
printf("%d\n", i);
}
int main(void)
{
static int static_var= 85;
static int static_var2;
static int x1 = 0;
static int x2 = 0;
int a = 1;
int b;
func1(static_var +static_var2 + a + b);
return a;
}
用gcc -c main.c -o main.o 编译成目标文件,用 size main.o可以从查看ELF文件的代码段、数据段和BSS段的长度 。
用objdump -h main.o 查看目标文件的结构和内容。
打印出文件的各个段后,你会发现多了一些段例如:.rodata只读段(%d\n) .comment存放的是编译器版本信息。
用objdump -s -d main.o -s以16进制的方式将段的所有内容打印出来, -d将所有包含指令的段返汇编。这样可以更加直观的看到目标文件中的结构和内容。注意,当声明一个变量 int a = 0; 后重新编译你的文件成目标文件后,当你用size命令打印出文件的各个段的大小的时候发现,你的.bss增加4个字节而.data 并没有增加,是因为编译器认为为初始化的都是0,编译器将它认为是为初始化,所以理所当然的将它存储在.bss段中。这样可以节省磁盘空间。这节先介绍到这里,会陆续更新。
浙公网安备 33010602011771号