C语言学习程序员

基础

2016-12-13  本文已影响32人  橙小汁


book.jpg

一个程序的运行环境包括:内存、运行库、系统调用

1、程序的内存布局

内存:是承载程序运行的介质,是程序进行各种运算和表达的场所。

Int *p = (int *)0x12345678;
++ *p;

上述代码展示了如何读写指定地址的内存数据。

Paste_Image.png

题外:q:程序经常出现段错误(segment fault)或者非法操作或者该内存地址不能read/write的错误,原因是:
A:1、程序员将指针初始化为NULL,之后没有给它一个合理的值就开始使用它;
2、程序员没有初始化栈上的指针,指针的值是一个随机数,之后就开始直接使用

2、栈与调用惯例

[1] 栈:后进先出,增长方向是从高地址到低地址
Paste_Image.png
esp代表栈顶,在栈上压入数据会导致esp减小,在栈上弹出数据会导致esp增大;直接减小esp的值代表开辟空间,直接增大esp的值代表回收空间。

栈保存了函数调用所需要维护的信息,称为栈帧,它包括:
(1)、函数参数及返回地址
(2)临时变量
(3)保存的上下文
题外:

 Int main()
{
     Char p[12];
}

如上是栈上的未初始化的空间,它在内存区域被设置为0xcc(烫)
若是堆上的未初始化的空间,它在内存区域被设置为0xcd(屯)

[2] 调用惯例

调用惯例包括:(1)函数参数的传递顺序和方式(2)栈的维护方式(3)名字修饰策略
函数返回值的传递:
(1)<=4字节--->eax
(2)<=8字节--->eax+edx
(3)>8字节 eg:


Paste_Image.png

(1)Main函数在栈上开辟了一块空间,将这块空间的一部分作为传递返回值的临时对象(temp)
(2)将temp的地址作为隐藏参数传递给return_test函数
(3)Return_test函数将数据拷贝给temp对象,并将temp对象的地址用eax传出
(4)Return_test返回之后,main函数将eax指向的temp对象的内容拷贝到n

题外:

cpp_obj return_test()
 {
      Return cpp_obj();
  }

构造一个cpp_obj()对象需要调用一次cpp_obj的构造函数,在返回这个对象的时候,还会调用cpp_obj的拷贝构造函数,c++返回值优化可以将这两步合并为一步,直接将对象构造在传出时使用的临时对象上,因此减少了一次复制过程。

3、堆与内存管理

[1] 堆:地址增长方向是从低地址到高地址
Int main()
 {
     Char *p = (char *)malloc(1000);
     Free(p);
 }

如上使用malloc申请了1000个字节的堆上空间,使用free释放掉

Linux提供了两种堆空间的分配方式:
(1)brk 设置进程数据段的结束地址
(2)mmap 向操作系统申请一段内存空间
Glibc的malloc函数是这样处理用户的空间请求的,对于<128kb的请求来说,会在现有的堆空间里按照堆分配算法分配一块空间并返回;对于>128kb的请求来说,会先使用mmap()函数为它分配一块匿名空间,然后在这个匿名空间中为用户分配空间;

[2] 堆的分配算法

1、空闲链表

Paste_Image.png

如上所示,在堆里的每一个空闲空间的开头有一个头结构(包括next和prev,即上一个和下一个空闲块的地址),也就是说,所有的空闲块形成了一个链表。
但是在释放空间的时候,并不知道空闲块的大小,此时一般的做法是在程序员申请K大小字节的空间时,其实际上是申请了K+4字节大小的空间,多出来的4字节用于存放空闲块的大小,这样就可以知道释放的内存块的大小。
但一般很容易越界造成对链表或者多出来的4字节的破坏,所以这种分配方式有一定的缺陷。
2、位图


Paste_Image.png

位图的核心思想是将整个堆划分为固定大小相同的块,当程序员申请内存时,总是分配整数个块给程序员。
优点:速度快,稳定性好,块不需要额外信息,易于管理
缺点:容易产生内存碎片

3、对象池
详见apache的common-pool工具库

系统调用
Linux:使用0X80号中断作为系统调用的入口
Windows:使用0x2E号中断作为系统调用的入口

系统调用原理
中断有两种类型:
一种称为软中断:通常是由一条指令引起。
一种称为硬中断:由硬件异常引起,比如电源断电或者键盘被按下。

//////////////////////////////////////////////////////////////////////////////////////////////////////

4.回到程序的原始

预处理阶段:
1.展开所有的宏定义,消除#define
2.处理所有的条件编译指令
3.处理以字符“#include”开头,将相应的头文件.h的内容直接插入到程序文本中
4.删除所有的注释
5.添加行号和文件名标识
6.保留所有的#pragma编译器指令
之后得到了一个以.i作为文件扩展名的另一个c程序。

编译阶段:编译器对上一步得到的.i文件进行一系列的词法分析,语法分析,语义分析以及优化后得到汇编语言程序.s。

汇编阶段:汇编器将上一步得到的.s转换成机器语言指令,并将这些指令打包成一种叫做可重定位目标程序的.o文件。

链接阶段:由上图知,hello.c程序调用了printf函数,而printf函数存在于一个名为printf.o的单独编译好了的 目标文件中。所以链接器负责将printf.o文件以某种方式合并到hello.o程序中,最终得到一个可执行目标文件.exe,它可以被加载到内存中,由系统执行。

//////////////////////////////////////////////////////////////////////////////////////////////////////

5.链接

动态链接
静态链接虽然使得人们可以开发属于自己的程序,但也带来了一系列的缺点,比如:
(1)浪费内存和磁盘空间,因为静态链接是在编译时加载,所以当程序要用到该库函数时,都要提前加载好这些静态库,这样会造成很大的空间浪费;
(2)模快更新困难。
动态链接为了解决这个缺点,将链接这个过程推迟到了运行时刻,会使得程序的升级变得而更加容易,只是相对性的提高程序的兼容性,比如在WINDOWS下会遇到如下问题:
Dll:即动态链接库,相当于Linux下的共享对象,windows下的dll和exe文件都是PE格式的二进制文件。

DLL HELL(DLL噩梦)
起因:
(1)使用一个旧版本的DLL替代一个新版本的DLL
(2)由于新版本的DLL函数无意发生改变
(3)由于新版本的DLL安装引入一个新BUG
预防:
(1)静态链接
(2)防止DLL覆盖
(3)避免DLL冲突

提高动态链接的速度
动态链接比静态链接慢的原因有:
(1)动态链接对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址;
(2)动态链接的链接工作在运行时完成。
鉴于此,我们采用延迟绑定来缓解动态链接速度较慢的现象,基本思想就是当函数第一次被用到时才进行绑定,不用时便不进行绑定。

//////////////////////////////////////////////////////////////////////////////////////////////////////

6.区分程序和进程

程序和进程的区别
程序:是一个静态概念,是预先编译好的指令和数据集合的一个文件。
进程:是一个动态概念,是程序运行时的一个过程。

装载的方式:
(1)静态装载的方式:将程序运行所需要的指令和数据全部装入内存;
(2)动态装载的方式:根据程序运行时的局部性原理,将程序最常用的部分驻留在内存,将不太常用的存在磁盘里。动态装载分为两种方法,分别是覆盖装入和页映射。

进程的建立
(1)创建虚拟地址空间:页映射函数是虚拟空间到物理内存的映射关系
(2)读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
(3)将cpu指令寄存器设置成可执行文件入口,启动运行:通过此,将控制权转交给进程。

当上面三步执行完成后,可执行文件真正的指令和数据并没有被装入内存,操作系统只是通过可执行文件的头部信息建立起可执行文件和进程虚存之间的映射关系。假设程序访问的页面是个空页面,则此时会发生页错误,这个时候cpu将控制权交给操作系统,操作系统通过查找上述第二部建立的数据结构找到这个空页面对应的VMA(LINUX中将进程虚拟空间中的一段叫做虚拟内存区域(VMA)),并计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中的虚拟页与分配的物理页面建立映射关系,最后将控制权交给进程,进程从刚才页错误的位置重新开始。

上一篇下一篇

猜你喜欢

热点阅读