备用面试题
作者:互联网
1、C程序代码编译、运行全过程解析
很多嵌入式初学者,不明白一个简单的C语言程序,是如何通过一步步编译、链接变成一个可执行文件的,程序到底是怎么运行的?运行的过程中需要什么环境支持?
今天就跟大家一起捋一捋这个流程,搞清程序编译、链接、加载、运行的整个脉络,以及程序在运行过程中的内存布局、堆栈变化。
- 程序的编译、链接过程
就以hello.c为例:从一个C语言源文件,到生成最后的可执行文件,基本流程如下:
C 源文件:编写一个简单的helloworld程序
预处理:生成预处理后的C源文件 hello.i
编译:将C源文件翻译成汇编文件 hello.s
汇编:将汇编文件汇编成目标文件 hello.o
链接:将目标文件链接成可执行文件
为了加深对这个过程的理解,我们可以在Linux环境下面,通过gcc命令精确控制每一个编译、链接过程
$ gcc -E hello.c > hello.i //会生成预处理后的C源文件hello.i
$ gcc -S hello.i //将hello.i编译成汇编文件hello.s
$ gcc -c hello.s //将汇编文件hello.s汇编成hello.o
$ gcc hello.o -o hello //将目标文件链接成可执行文件hello
$ ./hello // 运行可执行文件hello
- 程序的执行过程
当我们在shell交互环境下敲击 $ ./hello,这个hello程序到底是怎么运行的呢?
很简单。shell会首先通过系统调用fork创建一个子进程,然后从磁盘上将可执行文件hello的代码段、数据段加载(map)到这个子进程的地址空间内,接下来,在操作系统调度器的调度下,各个进程轮流占用CPU,就可以直接执行了。
- 在操作系统层面,对于每一个进程,在内核中都会有一个task_struct的结构体来描述它,里面存储进程的各种信息,各个结构体构成一个链表,操作系统通过调度器来轮流执行每个进程,如上图所示。
- 进程的虚拟空间和物理空间
- 每个进程使用的都是虚拟地址,地址空间0~4G,都是相同的。但是CPU在实际执行过程中,对于每个进程相同的虚拟地址,会映射到物理内存中的不同位置。每个进程都有自己的进程页表,在这个页表里有该进程虚拟地址和物理地址的对应关系。
- CPU内部有一个叫MMU的硬件部件会根据这个映射关系,直接将虚拟地址转换成物理地址,如下图所示。
使用虚拟地址的好处之一就是:为每个进程提供一个独立的、私有的物理地址空间,保护每个进程的空间不会被其它进程破坏。同时通过MMU对内存读写权限进行管理、保障系统的安全运行。如下图所示,每个进程在我们的物理内存(DDR)上,都有各自独立的内存空间:一个进程崩溃了,一般情况下,不会影响到系统,不会影响到其它进程的运行。
- 进程栈
-
栈是C语言运行的基础。没有栈,C语言函数是无法运行的:这是因为函数调用过程中的返回地址、参数传递、函数内的局部变量都是在栈中存储的,没有栈,C语言函数就无法运行。
-
Linux进程中的代码也是由一个个函数组成的,所以在运行进程之前,我们要首先初始化栈,如下图所示:
-
在程序运行过程中,通过栈指针,我们就可以将函数内的局部变量、返回地址保存在栈中。随着函数不断地调用、函数退出,而不断地入栈、出栈。
-
栈是一种数据结构,CPU的寄存器一般来讲,在设计的时候,会自动入栈出栈、自动增减栈的地址。比如ARM中的入栈出栈操作,当我们使用push/pop入栈出栈的时候,CPU的寄存器SP,即栈指针会自动增减地址,一直指向栈顶,这些都是指令集的实现,即CPU内部硬件电路的实现。
- 用户栈、内核栈、中断栈
-
在Linux环境下,进程一般分为两种,用户态进程和内核态进程。甭管是什么态,只要你是C语言,运行C代码就必须指定栈,否则C代码就无法运行。所以栈又分为用户栈和内核栈。
-
用户栈的虚拟地址空间在用户空间,内核栈的地址在内核空间。它们都是虚拟地址,最后通过MMU映射到物理内存的不同区域。
-
有时候,你还会看到中断栈的字眼,千万别被它吓到。中断程序、中断函数也是C语言,也是妖他妈生的,想运行中断处理程序也必须需要栈的支持,一般这种栈叫做中断栈。它可以使一个独立的中断栈,也可以占用进程栈的空间,跟进程栈共享。
- 小结
- 以上只是简单介绍一下一个C语言从编译、链接、运行、到进程创建、内存堆栈的大致流程。实际过程比这个更复杂一些、更深一些,限于篇幅的关系,很多细节无法一一细讲。
参考:C语言编译链接加载过程
2、变量,常量,静态变量存储的位置
常见的存储区域可分为:
1、栈
由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
2、堆
由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,程序会一直占用内存,导致内存泄漏,在程序结束后,操作系统会自动回收。
3、自由存储区
由malloc等分配的内存块,它和堆是十分相似的,不过它是用free来释放分配的内存。
4、全局/静态存储区
全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
5、常量存储区
这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)。
- 今天开始看《程序员的自我修养:链接、装载与库》,对ELF文件格式内容进行一下总结,主要分析全局变量,静态变量以及局部变量存放位置。
- ELF文件有很多种:可重定位文件(如静态库),可执行文件,共享文件(动态库),核心转储文件(Core dump file)。
- ELF文件主要有以下段:
.file header
.text section
.data section
.bss section
这里主要分析以下每个字段的内容。
file header字段里存放了描述整个文件的基本属性信息的内容,如程序入口地址,其他各段信息(偏移量和范围)
.text section:主要是编译后的源码指令,是只读字段。
.data section :初始化后的非const的全局变量变量或者局部static变量。
.bss:未初始化后的非const全局变量和局部static变量。
另外,还有一些其他字段:如.rodata字段和.comment字段分别存放只读数据和注释部分。
用书上提供的例子做测试:
#include <stdio.h>
int global_init_val = 84;//.data
int global_uninit_val;
char b[]="aaa";//.data
char c[]="dddd";//.data
const char e[]="yyyy";//g++:not found gcc:.rodata
const int a = 0x555555;//g++:not found gcc:.rodata
void func1(int i)
{
char * a ="abab";//.rodata
const char c[] = "eeee";//.text(len >3)
char b[] = "ddd";//.rodata(len <=3)
char d[] = "xxxx";//.text(>3 ?)
printf("%d/n",i);
}
int main()
{
static int static_var = 85;//.data
static int static_var2;
int a = 1;
int b;
func1(static_var+static_var2 + a+b);
return a;
}
经过objdump测试.o文件:
所有的初始化后的非const的全局变量变量或者局部static变量都放在.data段
而在g++下:
- const的全局变量或者static变量则不可见(猜想可能是编译时作为优化存放在寄存器中 ?)
在gcc下:
- const的全局变量或者static变量存放在.rodata和.text中,都是作为只读变量来存放的。
参考:全局变量,静态变量以及局部变量存放位置
参考:
参考:
标签:面试题,变量,C语言,static,内存,进程,hello,备用 来源: https://blog.csdn.net/juluwangriyue/article/details/118500172