注: 这是markdown原始文稿,无法看到图片,富文本格式文章见我的博文。
这两周读了日本作者矢泽久雄写的[《程序是怎么跑起来的》][1],解开了我这个作为通信专业的软件从业者的很多困惑,为了避免日后遗忘,将一些看了这本书之后的问题的解答记录下来。
Q:电脑的CPU中包含哪些部分?各自的作用有哪些?
A:CPU包含寄存器,控制器,时钟和运算器四种主要的结构。
- 控制器负责将内存上的指令、数据等读入到寄存器,并根据运算的结果控制整个计算机;
- 寄存器用来暂存数据、指令等处理对象,一般CPU包含20~100个不同的寄存器;
- 时钟负责CPU开始计时的时钟信号;
- 运算器负责运算从内存读入寄存器的数据
从程序员的角度来说,CPU可以看作寄存器的集合。CPU中包含不同种类的寄存器,各自有不同的功能。
Q:一个典型的C语言源代码在电脑中运行的基本流程是怎样的?
A:C语言写成的源代码是高级语言程序,但是CPU运行的代码是本地机器语言,因此C的源代码并不能立即运行。实际上,一个C的源代码需要经过编译、和链接生成.exe的可执行文件之后,电脑会将.exe文件的副本复制到内存中再运行。
Q:内存内部结构如何?内存的数据存取都有哪些数据结构?
A:内存是计算机的主存储器,通过芯片与计算机相连,主要负责存储指令和数据,CPU通过基址寄存器和变址寄存器读取和写入内存中的数据。内存由连续的长度为8bit(1个字节)的基本元素构成,程序启动之后CPU的控制寄存器根据时钟信号从内存中读取指令和数据。
存取内存的数据结构包括数组、栈、堆、队列、链表和二叉树。我们可以通过指针直接访问和改变对应内存地址中的变量的数值。
- 数组是多个同样类型的数据在内存中连续的排列的形式,可以通过数组的索引访问数组元素;
- 栈可以不通过指定地址和索引对数组元素进行读写。栈由栈底、栈顶描述,一般用来临时保存运算过程中的数据、连接在计算机设备上或者输入输出的数据;
- 队列与栈相似,栈的元素是FILO,但是队列是FIFO,队列一般用环形缓冲区实现;
- 链表与数组不同,它在内存中不是连续存储的,每个元素都有一个直接后继,像串珠一样将每个元素串联起来,最大优势是增减元素方便快捷;
- 二叉树中除了最终的子节点之外,每个元素都有两个后继结点,有序二叉树使得搜索变得更有效
Q:数据和程序是如何保存在计算机中的?
A:程序和数据是保存在计算机的硬盘中的,但是程序运行需要将机器语言的程序加载到内存,因为CPU的程序计数器指定内存地址才能读出程序内容。内存和磁盘因为自身特点的差异,它们之间具有紧密的联系。
- 磁盘缓存。由于磁盘的读取速度较慢,为了加快程序的运行,将磁盘中的部分数据加载到内存中缓存起来,之后在访问同一个数据的时候就直接从内存中读取数据,这样的机制叫**磁盘缓存**;
- 虚拟内存。**虚拟内存**刚好与之相反,在运行比较大的程序或者内存资源比较紧张可以将部分磁盘当作**假想的内存**来用。实现虚拟内存机制需要在磁盘为内存预留空间,并在程序运行时与内存中的内容进行置换(swap),window中提过**分页式虚拟内存机制**。一般虚拟内存的大小与内存相当或者是内存的两倍。
Q:什么是动态链接和静态链接?二者有何不同?
A:DLL(Dynamic link libary)是在程序运行时候动态加载的文件。
所謂動態链接,就是把一些經常會共用的程式碼(靜態链接的OBJ程式庫)製作成DLL檔,當執行檔呼叫到DLL檔內的函數時,Windows作業系統才會把DLL檔載入記憶體內,DLL檔本身的結構就是可執行檔,當程式需求函數才進行链接。透過動態链接方式,記憶體浪費的情形將可大幅降低。
简单来说,已经编译成汇编语言的程序文件,在进一步链接时如果直接将库文件链接进exe可执行文件,则该链接文件就是静态库,如果仅仅在程序运行时才进行链接称为动态链接,链接的目标文件就是动态链接库(windows中为dll文件)。需要说明的是,在链接之后,exe文件中包含了静态链接库的所有内容,所以会比较大,而动态链接库相对轻巧,并且**动态链接库可以在被多个同时运行的程序所共有,并且保证内存中只有一个dll文件中调用函数的副本**,这样就节省了程序运行的空间。实际上window操作系统的大部分API目标文件是动态链接库,动态链接库一般由导入库导入,导入库中并不存在目标函数的实体,仅仅保存目标函数所在的动态链接库的名称及路径。
关于动态链接和静态链接的详细介绍请参考博文[C++静态库与动态库](http://www.cnblogs.com/skynet/p/3372855.html "C++静态库与动态库")。
**Q:一个C语言源程序是如何变成可执行文件(exe)的?又是如何在操作系统中运行的?**
**A**:这是个比较大的问题,作者在书中举了个C语言的例子。大体来说,C的源程序需要通过编译器编译成汇编语言(asm文件),进一步链接需要的库文件(dll文件)生成可执行文件(exe文件),最后点击exe将可执行文件导入内存运行程序。以Sample.c文件为例
#include <stdio.h>
#include <windows.h>
char *title = "messgae box";
double average(double a, double b)
{
return (a + b)/2.0;
}
int WINAPI WinMain(HINSTANCE h, HINSTANCE d, LPSTR s, int m)
{
double ave;
char buff[80];
ave = average(123,456);
sprintf(buff, "average value is %f", ave);
MessageBox(NULL, buff, title, MB_OK);
return 0;
}
1. **编译**该文件,在源文件目录上运行命令`bcc32 -W -c Sample.c`,生成sample.obj目标文件;
2. **链接**需要的库文件,运行命令`ilink32 -Tpe -c -x -aa c0w32.obj Sample.obj, Sample.exe,, import32.lib cw32.lib`
需要说明的是,c0w32.obj文件是与所有程序起始位置相结合的处理内容,称为程序的**启动**。在源程序中,我们**调用**了系统函数sprintf和messagebox,因此,需要将这两个函数对应的库函数(其中的内容与exe文件相同,都是本地代码)**链接**进来,告诉链接器去哪里找这两个函数对应的本地代码。
sprintf的本地代码在cwlib32.lib中,编译之后会将它的目标函数合成到exe文件中,称为**静态链接**;而messagebox的本地代码在库文件user32.dll里,使用import32.dll是为了告诉连接器“messagebox在库文件user32.dll中,以及user32.dll在哪里”,所以import32.dll称为导入库。程序运行时,执行从DLL文件调出的MessageBox()函数这一信息就会和exe文件结合,称为**动态链接**。
**Q:可执行文件包含哪些内容?它加载到内存中是什么样子?**
**A**:可执行文件中包含了源程序的变量和函数的虚拟地址,在加载到内存之后需要必要的信息将虚拟地址转换成实际地址,转换需要的信息就在exe文件开始的部分,称为再配置信息。exe文件被加载到内存之后,就将这些虚拟内存转换成实际内存,程序运行中会生成栈和堆,因此在内存中的样子如下图所示
[![3gXgTTNBST7Ua27e9SaE2B5d.jpg](https://s20.postimg.org/ttpdsl5rx/3g_Xg_TTNBST7_Ua27e9_Sa_E2_B5d.jpg)](https://postimg.org/image/bqwb1d9x5/)
**Q:c,o,a,lib,obj,dll这些文件分别是什么?他们之间是什么关系?**
**A**:c是C语言的源文件,如博文[Linux的.a、.so和.o文件](http://blog.csdn.net/chlele0105/article/details/23691147 "Linux的.a、.so和.o文件") 中所述
> lib,dll,exe都算是最终的目标文件,是最终产物。而c/c++属于源代码。源代码和最终目标文件中过渡的就是中间代码obj,实际上之所以需要中间代码,是你不可能一次得到目标文件。比如说一个exe需要很多的cpp文件生成。而编译器一次只能编译一个cpp文件。这样编译器编译好一个cpp以后会将其编译成obj,当所有必须要的cpp都编译成obj以后,再统一link成所需要的exe,应该说缺少任意一个obj都会导致exe的链接失败。而 .o,是Linux目标文件,相当于windows中的.obj文件,.so文件为共享库,是shared object,用于动态连接的,相当于windows下的dll,.a为静态库,是好多个.o合在一起,用于静态链接
**Q:什么是_BSS段和_DATA段?全局变量和局部变量在程序运行时有何不同?**
**A**:这是汇编语言的概念,编译器将高级语言源程序转换成汇编文件(.asm文件),有如下的源文件sample2.c
int AddNum(int a, int b)
{
return a + b;
}
void MyFun()
{
int c;
c = AddNum(123,456);
}
经过编译之后的汇编文件(软件环境win10,gcc编译器)内容如下:
```
.file "sample.c"
.text
.globl _AddNum
.def _AddNum; .scl 2; .type 32; .endef
_AddNum:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl 12(%ebp), %eax
addl %edx, %eax
popl %ebp
ret
.globl _MyFun
.def _MyFun; .scl 2; .type 32; .endef
_MyFun:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
movl $456, 4(%esp)
movl $123, (%esp)
call _AddNum
movl %eax, -4(%ebp)
leave
ret
.ident "GCC: (tdm-1) 4.9.2"
```
汇编程序最接近机器语言,而且其与C语言一一对应,所以通过汇编文件就可以了解程序运行的大体情况。从上面的汇编文件,可以看到如下的结果
1. 寄存器esp指向栈顶元素地址,每个元素占据4个字节的数据;
2. 在每个函数开始的时候,都要将寄存器ebp的数据压入栈中进行保护;
3. 上述程序中隐藏的一个关键步骤是在第21行,call AddNum时,计算机已经将MyFun函数的下一个指令的地址压入栈中,在调用完AddNum时(第12行),返回函数Myfun时候会自动将栈中的返回指令的地址出栈交给CPU的程序计数器,这样就可以实现在调用函数之后仍然返回原来的调用的地方;
4. 函数的入参被保存在栈中,返回值被保存在寄存器里。
```c
int a;
int b;
float fl;
int c = 9;
int d = 10;
int e = 11;
int f = 12;
void MyFun(void)
{
int a1,b1,c1;
float fl1;
a1 = 1;
b1 = -1;
fl1 = -99.34;
c1 = -87;
a1 = a;
b1 = b;
fl1 = fl;
c1 = c;
}
```
以上的C源代码转换成汇编语言是
```
.file "sample2.c"
.comm _a, 4, 2
.comm _b, 4, 2
.comm _fl, 4, 2
.globl _c
.data
.align 4
_c:
.long 9
.globl _d
.align 4
_d:
.long 10
.globl _e
.align 4
_e:
.long 11
.globl _f
.align 4
_f:
.long 12
.text
.globl _MyFun
.def _MyFun; .scl 2; .type 32; .endef
_MyFun:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl $1, -4(%ebp)
movl $-1, -8(%ebp)
movl LC0, %eax
movl %eax, -12(%ebp)
movl $-87, -16(%ebp)
movl _a, %eax
movl %eax, -4(%ebp)
movl _b, %eax
movl %eax, -8(%ebp)
movl _fl, %eax
movl %eax, -12(%ebp)
movl _c, %eax
movl %eax, -16(%ebp)
leave
ret
.section .rdata,"dr"
.align 4
LC0:
.long -1027166700
.ident "GCC: (tdm-1) 4.9.2"
```
从中可以看出全局变量保存在.comm和.globl段,局部变量保存在寄存器中,因此在程序运行的整个过程中,全局变量可以随时访问,但是局部变量却会在用过之后消失。
关于windows的汇编的内容可进一步参考文章[汇编与逆向分析](http://www.mouseos.com/assembly/index.html "汇编与逆向分析")
---
以上是此书最干货的部分,书中该介绍了计算机二进制数,和计算机硬件的部分内容,再次略过不提。