关闭

一个小程序背后的故事——分段

标签: 汇编cache编译器优化compilation语言
398人阅读 评论(0) 收藏 举报

 现在大家都在谈设计,谈架构,谈框架,套用一句流行语:“ 虽然听不懂他们在说什么,但是感觉很牛的样子。 

   

    诚然。在软件日益庞大的今天,先古时代单打独斗的天才程序员几乎销声匿迹了,程序员的成功大多是团队的成功,团队合作无非也是分层,分模块,架构设计完了就是用各种框架,高内聚,低耦合云云。很少有人听到有人在讨论编译,连接,算法,数据结构之类的东西。程序员越来越像软件集成员了。


我还是喜欢自下而上的学习方法。前天看了本书《程序员的自我修养——编译,连接和库》。又明白了一些底层的东西。准确讲应该是操作系统和高级语言(应用程序)之间那层。这本书很好。(只有在买书的时候,我才能深刻的体会到知识就是金钱。65块钱啊。才450多页。)大一的时候借了,“ 虽然看不懂作者在讲什么,但是感觉很牛的样子,大二的时候也借了,依旧是“ 虽然看不懂作者在讲什么,但是感觉很牛的样子,前几天在图书馆溜达。又一次和它邂逅。遂收入囊中,借来看看。虽然不至于看懂,但令我欣慰的是终于知道作者是讲什么的了。


初中时候觉得,写软件的人好厉害啊。高中的时候觉得,写操作系统的人好厉害啊。大一的时候觉得,写编译器的人真牛啊。大二的时候想,发明语言的才是真正的大牛。现在想想,只能对以前的想法呵呵一笑。

John Resig 22岁写了jquery 。唉各种你懂的。

先说一点编译的东西吧。


编译过程:

1.预编译prepressing展开宏#define,加载库文件#include等。

2.编译compilation 高级语言编译成汇编语言,这个过程会有编译器优化。

3.汇编assembly汇编语言编译成二进制代码。这个过程是最简单的。

4.连接linking地址和空间分配,符号决议,重定位。

个人觉得编译和连接过程是最复杂的。


编译:

    因为现代CPU已经很繁杂,各种功能很多。暂且不说CISC 和 RISC X86X86_64IA64MIPSARM等等架构的CPU,还有乱序发射,流水线优化,多核优化,寄存器优化,cacha优化等等一堆乱七八糟的东西。可谓眼花缭乱。记得GCC支持50+CPU平台的编译。恐怖的一米。乱序发射,编译器,cache优化等各种CPU的特性,也增加了程序并发多线程编写的难度。


连接:

    动态连接,静态连接,可重入,地址重定向,个人感觉也是比较难的东西。

再穿插点X86和汇编的东西。8086处理器有20跟地址线,但是只有16跟数据线。所以可以定位1M的内存空间,但是只能访问64K的地址,为了解决这个问题,Intel 的工程师很聪明的用了一个方法,段+偏移量 这种内存访问方式。写汇编的时候,一般是这样开头的。

Code segmentdata segmentstack segment 三个段。我以前一直以为没有操作系统的时候,这三个段就依次导入到8086的三个段里面。真无知啊。

    冯诺依曼果然是大神。没有计算机的时候就设计了计算机的灵魂。数据和指令分离。试想一下在混沌初开的时候,如果数据和指令是混杂在一起的话,那将是多么恐怖的事情。不过只要是一个正常的人,写汇编的时候就会很自然的把指令和数据分离。


假如不分离的话,一个指令执行完了,后面一个是数据,要怎么区分?CPU会不会把数据当做指令去执行?


解决方法之一是内存里面加标志位,0是可执行指令,1是不可执行的数据。无疑这会造成内存浪费。


另一种方法是按照 指令-数据-指令-数据-指令-数据 交叉存放(当然,也可以1指令n数据,或者n指令1数据)方式存放,CPU根据周期来判断内存内容是数据还是指令,这会出现很严重的问题,1.程序指令和数据的比例不是固定的。2.又会增加CPU和程序在二进制上的兼容难度。(关于二进制兼容,可以百度一下。)


上面一个问题就很麻烦了,姑且放在一边不考虑,如果程序员无意写错了指令,跳转到了一个存放数据的内存或者不小心修改了指令(现在的指令都是只读的,这也就是为什么每次调试的时候要编译整个程序)怎么办,那从那以后所有的程序都是坏死的(起码是不可预料的),现在我们写程序,错一句就那么一句错了,不会影响到剩下的,如果一句错了,剩下的都跟着乱了,那将是多么恐怖的事情。

所以,一个正常的人会把指令和数据分离的。冯大神果然是大神。


指令和数据混杂的坏处可谓多多,指令和数据分离有什么好处呢?可也是谓多多。


1.现代CPUcache都是指令cache和数据cache分离的,想象一下,数据和指令分离了,指令cache和数据cache也分离了,那么指令和数据的cache命中率一下子提高了50%,一个小时的程序半个小时就能执行完。这还没算上面两个问题的内存节约以及兼容性问题。

2.指令是只读的,避免了程序员无意识地对指令区域内存的修改。避免产生了很多错误。

3.指令和数据分离,所以指令可以重复利用。也就是后面将要讨论的静态连接以及动态链接的东西。


    综上所述,计算机要指令和数据分离。我估计冯大神的论文也就这么写的内容吧。改天到网上找来膜拜一下。


    冯骨开天之后,程序猿们看到了光明。沐浴在神的思想之中,程序猿们感到了快乐。可是有些程序猿不满足于神既有优化,于是有猿想:“数据中也有只读的和可修改的,我是不是可以把数据也分下段?这样,我写程序时如果手误修改了只读的内容,那么程序会报告错误。这该是多么美妙的事情啊。”于是就有了那个经典的程序:


#include<stdio.h>

int main(int argc ,char* argv[]){

char *str = "hello";

*str = 'a';

printf("$s",str);

return 0;

}


好多懂一点的C语言新手可能会以为输出结果是 "aello" ,但是实际结果是:


Linux: core dump 吐核,我一直觉得吐核这个翻译是个很Cute的翻译。其实也是段错误,陷入内核。终止程序,把控制权交给内核。应该是个CPU异常中断或者错误中断。

Windows 下面好像是段错误。既对只读段进行赋值操作的段错误。

所以,正确的写法是:

#include<stdio.h>

int main(int argc ,char* argv[]){

char *str []= "hello";

*str = 'a';

printf("$s",str);

return 0;

}

应该用字符串数组。字符串编译完了以后是放在数据段的只读区域的。而字符串数组是放在程序上下文中。既代码段。(有同学可能会问,这不是数据么。怎么去了代码段?额。这个我也很有点迷茫。)但是执行到printf("$s",str);的时候,是进行了n次压栈操作,一方面保存当前寄存器(程序上下文)以及其他要保存的信息,另一方面把参数(str指针)压栈,传递给printf函数。这个过程有个准业术语,叫调用惯例(参数传递方式-栈、寄存器、内存 堆维护方式,参数顺序,名字修饰符策略等)也是一套标准,详细情况请自行百度。好。在这儿打住。如果这样写:


#include<stdio.h>

char *str []= "hello";

*str = 'a';

int main(int argc ,char* argv[]){

printf("$s",str);

return 0;

}


"hello" 和 str 是放在数据段里了。


    刚才说到分段,无到有是冯大神指出的光明道路,那么有到多则是无数程序猿的智慧总结。


    自从有程序猿发现可以根据实际需求来再次分段之后,各种分段就如雨后春笋般破土而出。


    比如注释信息段,调试段,编译器信息段,目标平台段,动态连接信息段,符号哈希表段,等等。Linux 下面有查看可执行文件(以及so动态/静态链接库)的工具包:binutilswindows下面也有。自己百度。


    马克思告诉我们,事物总是不断发展的。各种段之后,慢慢的内存大了,有了内存管理的概念,分段的好处是内存可以充分利用。比如1M的内存划分为512个段,内存利用更充分了。再后来慢慢的有了OS的概念,于是又有了堆(heap)段的概念。又有了分页方式的内存管理,每次分配给程序的内存颗粒更小了,所以内存利用率得到了极大的提高。既:生产力气得到了极大的发展。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:23675次
    • 积分:329
    • 等级:
    • 排名:千里之外
    • 原创:6篇
    • 转载:6篇
    • 译文:1篇
    • 评论:5条
    文章分类
    最新评论