链接与库
其实学完C语言的语法后,我们往往会有数不清的疑惑,例如编译器在编译的时候就可以分配内存,那么不同的程序会不会分配到相同的内存地址,计算机如何处理这种冲突?C语言既然可以操作内存,我们能不能修改其他程序的内存数据,游戏外挂是不是这样实现的?程序是怎么被加载到内存的,C语言main函数又是谁调用的?为什么编译之后还要链接?什么是动态库什么又是静态库?
大家是否也曾和我有过一样的疑问呢?这些知识其实都分散在操作系统原理、编译原理、汇编语言等领域,零散而不成系统,只有C语言才能将之串联成一条线索,但遗憾的是,市面上并没有一本讲C语言的书能把这些串起来,成体系的而又深入浅出的讲解清楚。也几乎没有太多书能深入的回答我上面提出的那些疑问。要想弄清楚这些问题,可能需要阅读大量的书籍,学习相当多的基础知识,往往让人望而却步。然而这些知识又是编程领域的九阳神功,一旦练成,其他花招只不过是信手拈来。这里,我结合自身的学习经验,谈谈最重要的一些概念,如果希望对这些概念有深入全面的学习和理解,推荐一本好书《程序员的自我修养——链接、 装载与库》,这是目前唯一一本综合起来讲解这些知识的书,当然,可能还需要看以下书籍《深入理解计算机系统(原书第3版)》、《链接器和加载器》、《汇编语言:基于LINUX 环境(第3版)》
好了,学习C语言语法的资料汗牛充栋,千篇一律,但是真正的干货却不多,下面就开始我们这个系列文章真正的干货吧
虚拟内存
在早期的计算机系统中,程序是可以直接操作物理内存的,例如我们可以使用C语言,往某个内存地址如0xff0001
的空间写入数据,但是这样一样来,带来了许多无法避免的问题。比如说,程序A占用了绝大部分内存,那么再运行起程序B时,发现没有内存可用了,因此早期的系统是单任务的,同一时间不能运行多个程序,B程序想要启动,必须等到A程序运行结束,释放了内存之后,它才能启动。
随着计算机的发展,多任务的系统出现,同时可以运行多个程序,这时候带来新的问题,譬如内存重叠,内存冲突。假如编译器在编译程序A时,使用了0xff0001
这个空间,而程序B在编译时也使用了这个地址,这两个程序同时一运行不就冲突了吗?程序在编译的时候,永远无法确定哪个内存地址是没有被人使用的,就像你去超市储物箱存东西,你不可能事先选定一个编号的柜子去存。不仅如此,还会存在其他的问题,比如恶意程序可以随意修改其他程序正在使用的内存,假如0xff0001
是存的游戏中的金币数,那么其他任意程序都可以修改这个内存中的值,不存在软件安全可言。
为了适用计算机发展的需要,为了更高效的调度内存资源,为了解决以上的问题,操作系统出现了虚拟内存的概念。
关于虚拟内存的理解,这里举个形象的例子。游泳馆一般都有储物柜,没有虚拟内存之前,就相当于没人管理储物柜,你自己可随便去打开一个储物柜放东西,有时候你打开的柜子可能已经被别人使用了,这就造成了冲突。现在游泳馆升级了,有专门的人管理储物柜,你不能直接去存东西,要存东西,你只能去柜台上找服务员,你找到服务员说,把我的包存到10号柜子里面去。服务人员看了你一眼说必须登记,于是你把自己的手机号报给了她,这时候服务员在登记表上记下你的手机号,以及你指定的10号柜,然后找到一个空的23号柜子,于是记一笔,实际存放于23号。等到你去取包时,你跟服务员说取10号柜子的包,然后报一下手机号,服务员则根据登记记录帮你去23号柜取出来。在这个例子中,10号就是虚拟地址,23号就是物理地址。使用虚拟地址的一个好处就是我们可以拥有完整的地址空间,什么意思呢,简单说,如果游泳馆共用100个柜子,编号从1到100,那么我们可以任意的使用1~100个虚拟地址,对应程序而言,它根本不知道自己使用的是虚拟地址。
现代操作系统,都是建立在虚拟内存管理之上的。所谓虚拟内存,简单说就是一张地址转换表,就如同我们上面举例中的登记表。程序不能直接操作物理内存了,只能操作这张转换表。虚拟内存的这张表为了提升查找效率,采用了分页的方式,因此这张表也叫页表。
来看一下简单的示意图
程序实际上操作的是页表中的虚拟内存地址,而页表映射了物理内存地址。这个页表好比一本字典中的目录索引,当程序访问一个虚拟内存地址时,经过页表转换后,才对应到真正的物理内存中,因此程序就不需要再关注真正的物理内存。对程序而言,它的一切操作都是对虚拟内存地址进行的,由操作系统在底层再去转换真正的物理内存地址。
那么这样做有什么好处呢?
-
避免地址冲突。即使两个程序操作了一个相同的内存地址也不会冲突,因为程序操作的都是虚拟地址,操作系统可以根据这两个程序不同的进程,分别将两个相同的虚拟地址映射到不同的物理地址中储存。比如两个人都住在76号,但一个是霞飞路76号,一个是上海路76号,