共有回帖数  0  个 
	 
	
	
	
     
          
          
               
				
			 
				
					 
 
            
				   - 
						
						
							 
									写在前面的话 
     
    写本文的目的有: 
    1. 献给三跃妈 
    2. 笔者自认为至少将脱离技术道路若干年甚至从此脱离,写此文以作纪念 
    3. 目前的教科书有一些严重的问题影响了很多的人,至少笔者学习的道路比较坎坷 
    4. 希望本文能够给一些想了解嵌入式的有志青年 
    5. 计算机的书籍很少把一些东西搅合到一起说,讲操作系统的书专讲操作系统,讲 C 语言的书专讲语法,甚至有些程序员写了5年程序不能把自己的程序的来龙去脉讲清楚。C是把这些问题串到一起的切入点。 
     
    本文的标题也意味着其中没有过多的细节讲解,也不是一章讲解C语言的教程。(其实是笔者岁数大了懒得翻阅资料了,笔者已经30高龄了,希望大家谅解)。另外笔者希望读者有 C 语言的基础,了解至少一种 CPU 的汇编语言。 
     
    另外,目前关于 C 语言是不是过时的争论很多,我不想参与这些争论,正所谓萝卜白菜各有所爱��
囫囵C语言(一):可执行文件的结构和加载 
     
    看到这个标题很多人可能想,老家伙老糊涂了。可执行文件的结构和 C 语言有什么关系。 
     
    我们先来看一个程序: 
     
    ///////////////////////////////////////////////////////////////// 
     
    int global_a = 0x5; /* 01 */ 
    int global_b; /* 02 */ 
     /* 03 */ 
    int main() /* 04 */ 
    { /* 05 */ 
     char *q = "123456789"; /* 06 */ 
     /* 07 */ 
     q[3] = 'A'; /* 08 */ 
     /* 09 */ 
     global_a = 0xaaaaaaaa; /* 10 */ 
     global_b = 0xbbbbbbbb; /* 11 */ 
     /* 12 */ 
    // strcmp(q, NULL); /* 13 */ 
     return 0x0; /* 14 */ 
    } /* 15 */ 
     
    1. 你能说出程序中出现的变量和常量在可执行程序的哪个段中么? 
    2. 程序运行的结果是什么? 
     
    ///////////////////////////////////////////////////////////////// 
     
    能正确回答上面问题者,此节可以跳过不读: 
     
    如果有人问笔者第一个问题,笔者会响亮的回答:“不知道”!。因为你没告诉我目标 CPU,编译器,链接器。 
    如果有人问笔者第二个问题,笔者会更响亮的回答:“不知道”!。因为你没告诉我链接器,链接参数,目标操作系统。 
     
    比如 "123456789" 在某些编译环境下出现在 ".text" 中,某些编译环境下出现在 ".data" 中。 
    再比如,如果用 VC6.0 环境,编译时加上 /GF 选项,该程序会崩溃(第 8 行)。 
    再比如第 13 行,这种错误极为愚蠢,但是在某些操作系统下居然执行得挺顺利,至少不会崩溃(一种HP的UNIX操作系统上,可惜笔者没有留意版本号)。 
     
    所以 C 程序严重依赖于,CPU,编译器,链接器,操作系统。正是因为这种不确定性,所以为了保证你写的程序能在各种环境下运行,或者你想能够在任何环境下 debug 你的 C 程序。你必须知道可执行文件的格式和操作系统如何加载。否则当你在介绍自己的时候,只能使用类似:“我是X86平台上,VC6.0集成开发环境下的 C 语言高手” 之类的描述。颇为尴尬。 
     
    为了说明方便我们的讨论建立在一套虚拟的环境上。当然了这仅限于宏观的讨论,一些具体的例子我会给出我调试所用的环境。我们假设虚拟环境满足下列条件: 
    1. 足够物理内存 
    2. 操作系统不允许缺页中断 
    3. 物理页面 4K 
    4. 二级页表映射 
    5. 4G 虚拟地址空间 
    6. 操作系统不支持 swap 机制 
    7. I/O 使用独立的地址空间 
    8. 有若干通用寄存器 r0,r1,r2,r3,...... 
    9. 函数的返回值放在 r0 中 
    10. 单 CPU 
     
    (哈哈,没有具体的环境,我说错了也没人知道) 
     
    言归正传,过于古老的文件结构我们不提(入门的格式请参考 a.out 格式)。现在比较常用的文件格式是 ELF 和 PE/COFF。嵌入式方面 ELF 比较主流。 
     
     可执行文件基本上的结构如下图: 
     
     +----------------------------------+ 
     | | 
     | 文件头 | 
     | | 
     +----------------------------------+ 
     | | 
     | 段描述表 | 
     | | 
     +----------------------------------+ 
     | | 
     | 段1 | 
     | | 
     +----------------------------------+ 
     | | 
     | : | 
     | | 
     +----------------------------------+ 
     | | 
     | 段n | 
     | | 
     +----------------------------------+ 
     
     其中这些段中常见的段有 .text,.rodata,.rwdata,.bss。还有一些段因为编译器和文件格式有细微差别我们不再一一说明。 
     参考:1. Executable and Linkable Format Specification 
     2. PE/COFF Sepcification 
     
     .text:正文段,也称为程序段,可执行的代码 
     .rodata:只读数据段,存放只读数据 
     .rwdata:可读写数据段, 
     .bss段:未初始化数据 (下文详述) 
     
     有了虚拟的环境就好蒙了:就上面的例子来说,我们先回答第一个问题: 
     1. a 在 .rwdata 中 
     2. b 在 .bss 中 
     3. q 程序运行的时候从 stack 中分配 
     4. 'A',0x5,0xaaaaaaaa,0xbbbbbbbb,0x0 在 .text 段。 
     5. "123456789" 在 .rodata 中 
     
     第二个问题,程序在第 8 行会崩溃。程序为什么会崩溃呢?要回答这个问题我们要知道可执行程序的加载。 
     
     可执行程序的加载 
     
     当操作系统装载一个可执行文件的时候,首先操作系统盘但该文件是否是一个合法的可执行文件。如果是操作系统将按照段表中的指示为可执行程序分配地址空间。操作系统的内存管理十分复杂,我们不在这里讨论��
就上面的例子来说可执行文件在磁盘中的 layout 如下:(假设程序的虚拟地址从 0x00400000 开始,该平台的页面大小是 4K) 
     
     +----------------------------------+ 
     | | 
     | 文件头 | 
     | | 
     +----------------------------------+------------------ 
     | .text 描述 | ^ 
     | 虚拟地址起始位置 : 0x00400000 | | 
     | 占用虚拟空间大小 : 0x00001000 | | 
     | 实际大小 : 0x00000130 | | 
     | 属性 :执行/只读 | | 
     +----------------------------------+ | 
     | .rwdata 描述 | | 
     | 虚拟地址起始位置 : 0x00401000 | | 
     | 占用虚拟空间大小 : 0x00001000 | 
     | 实际大小 : 0x00000004 | 段描述表 
     | 属性 :读写 | | 
     +----------------------------------+ 
     | .rodata 描述 | | 
     | 虚拟地址起始位置 : 0x00402000 | | 
     | 占用虚拟空间大小 : 0x00001000 | | 
     | 实际大小 : 0x0000000A | | 
     | 属性 :只读 | | 
     +----------------------------------+ | 
     | .bss 描述 | | 
     | 虚拟地址起始位置 : 0x00403000 | | 
     | 占用虚拟空间大小 : 0x00001000 | | 
     | 实际大小 : 0x00000000 | | 
     | 属性 :读写 | v 
     +----------------------------------+----------------- 
     | | 
     | .text 段 | - 4K对齐,不满补 0 
     | | 
     +----------------------------------+----------------- 
     |0x5 | 
     | .rwdata 段 | - 4K对齐,不满补 0 
     | | 
     +----------------------------------+----------------- 
     |123456789 | 
     | .rodata 段 | - 4K对齐,不满补 0 
     | | 
     +----------------------------------+----------------- 
     
    请注意,.bss 段仅仅有描述,在文件中并不存在。为什么呢?.bss 专用于存放未初始化的数据。因为未初始化的数据缺省是 0,所以只需要标记出长度就可以了。操作系统会在加载的时候为它分配清 0 的页面。这种技术好像叫做 ZFOD (Zero Filled On Demand)。 
     
    操作系统首先将文件读入物理页面中(物理页面的管理比较复杂,不属于本文讨论的范围),反正大家就认为操作系统找到了一批空闲的物理页面,将可执行文件全部装载。如图: 
     
     : 
     +----------------------------------+ ---- 物理页面对齐 
     | | 
     | .text 段 | 
     | | 
     +----------------------------------+ 
     : 
     : 
     +----------------------------------+ ---- 物理页面对齐 
     |0x5 | 
     | .rwdata 段 | 
     | | 
     +----------------------------------+ 
     : 
     : 
     +----------------------------------+ ---- 物理页面对齐 
     |123456789 | 
     | .rodata 段 | 
     | | 
     +----------------------------------+ 
     : 
     : 
     
    在物理地址中,这几个段并不连续,顺序也不能保证,甚至如果一个段占用几个页面的时候,段内的连续性和顺序都不能保证。实际上我们也不程序关心在物理内存中的 layout。只需要页面对齐即可。 
     
    最后操作系统为程序创建虚拟地址空间,并建立虚拟地址-物理地址映射(虚拟地址的管理十分复杂,反正大就认为映射建好了。另外:注意我们的假设,系统不支持缺页机制和 swap 机制,否则没有这么简单)。然后我们从虚拟地址空间看来,程序的 layout 如下图: 
     
     +----------------------------------+ 0x00400000 
     | | 
     | .text 段 | 
     | | 
     +----------------------------------+ 0x00401000 
     |0x5 | 
     | .rwdata 段 | 
     | | 
     +----------------------------------+ 0x00402000 
     |123456789 | 
     | .rodata 段 | 
     | | 
     +----------------------------------+ 0x00403000 
     | | 
     | .bss 段 | 
     | | 
     +----------------------------------+ 
     
    同时操作系统会根据段的属性设置页面的属性,这就是为什么通常程序的段是页面对齐的,因为机器只能以页面为单位设置属性。 
     
    所以第二个问题自然就有了答案。程序会 crash。因为 .rodata 段所属的页面是只读的。其实有些编译器会将常量 "123456789" 放在 ".text" 中,其实是一样的,两个段都是只读的,写操作都会导致非法访问,甚至同一种编译器,不同的变异参数,这个常量也会出现在不同的位置。实际上这个保护由编译器,链接器,操作系统,CPU串通好了,共同完成的。 
     
    所以说计算机有些具体问题并没有一定之规,但是他们基本的原理是一样的。我们掌握了基本原理,具体问题可以具体分析。 
囫囵C语言(二):陷阱,中断和异常 
     
    上一章怀疑笔者老糊涂的读者,看到这个标题,基本上已经打消了疑虑:老家伙确实糊涂了。这三个概念和C语言有什么关系呢? 
     
    中断这个词恐怕人民群众都不陌生。很多人把中断分为两种:硬件中断和软件中断。其实怎么叫关系都不大,关键是我们要明白他们之间的异同点。 
     
    笔者本身比较喜欢把 “中断”,分为三种即陷阱,中断和异常,我似乎记得Intel是这么划分的(这句话我不保证正确啊,有兴趣的读者自己看一下 Intel 的手册)。他们的英文分别是 trap,interrupt 和 exception。 
     
    陷阱 (trap): 
    大家都知道,现代的CPU都是有优先级概念的,用户程序运行在低优先级,操作系统运行在高优先级。高优先级的一些指令低优先级无法执行。有一些操作只能由操作系统来执行,用户想要执行这些操作的时候就要通知操作系统,让操作系统来执行。用户态的程序就是用这种方法来通知操作系统的。 
     
    具体怎样做的呢?操作系统会把这些功能编号,比如向一个端口写一个字符的功能调用编号 12,有两个参数,端口号 port 和写入的字符 bytevalue。我们可以如下实现:(这个例子无法编译,但是这种汇编和 C 混合编程的风格微软的编译器支持,十分好用,顺便夸一句微软,他们的编译器是我用过得最优秀的商业编译器) 
     
    int outb(int port, int bytevalue) 
    { 
     __asm mov r0, 12; /* 功能号 */ 
     __asm mov r1, port; /* 参数 port */ 
     __asm mov r2, bytevalue; /* 参数 bytevalue */ 
     __asm trap /* 陷入内核 */ 
     
     return r0; /* 返回值 */ 
    } 
     
    在操作系统的 trap 处理的 handler 里面,相信大家已经知道怎么办了。有些敏感的读者可能已经明白了,原来一部分 C 的库函数是用这种方法实现的。 
     
    中断: 
    中断我们这里专指来自于硬件的中断,通常分为电平触发和边沿触发(请参考数字电路)。简单的说就是 CPU 每执行完一条都去检测一条管腿的电平是否变化。如果满足条件,CPU 转向事先注册好的函数。系统中最重要的一个中断就是我们经常说的时钟中断。为什么要说这个呢?这和 C 程序有什么关系呢?书上说了中断是由操作系统处理的,操作系统会保存程序的现场啊,用户程序根本感觉不到中断的存在啊。书上说得没错,但是它有两件事情没有告诉你: 
    1. 线程调度策略。 
    2. 程序的现场不包括什么? 
     
    这里笔者想插一句话表达对国内操作系统教材作者的敬仰,他们是怎么把操作系统拆成一块一块儿的呢?因为,进程管理,线程调度,内存管理,中断管理,IPC,都是互相关联的。笔者十分怀疑分块讨论的意义到底有多大。 
     
    言归正传,先回答第一个问题,线程调度时机。在哪些情况下操作系统会运行 scheduler 呢?现代操作系统调度的基本单位都是线程,所以我们不讨论进程的概念。 
     
    1. 一些系统调用 
    2. I/O 操作 
    3. 一个线程创建 
    4. 一个线程结束 
    5. mutex lock 
    6. P semaphore 
    7. 硬件中断 / 时钟中断 
    8. 主动放弃 CPU,比如 sleep(), yield() 
    9. 给另外一个线程发消息,信号 
    10. 主动唤醒另外一个线程 
    11. 进程结束 
    : 
    : 
    欢迎大家来电来函补充 (我记不住那么多了) 
     
    第二个问题,现场不包括什么。至少不包括全局变量。 
于是就有了一个经典的面试题: 
     
     
    int a; 
     
    void thread_1() 
    { 
     for (;;) 
     { 
     do something; 
     a++; 
     } 
    } 
     
    void thread_2() 
    { 
     for (;;) 
     { 
     do something; 
     a--; 
     } 
    } 
     
    main() 
    { 
     create_thread(thread_1); 
     create_thread(thread_2); 
    } 
     
    现在大家应该明白这种写法的错误了吧。因为 a++,a--,并不是一条汇编语言,它会被中断打断,而中断又会引起线程调度。有可能将另外一个线程投入运行。所以结果是无法预测的。讨论这个问题的文章很多,笔者也就不多费口舌了。 
     
    提个思考题,操作系统内部,中断和中断之间,中断和线程之间,怎么保护临界资源的呢?多个 CPU 之间呢? 
     
    异常:exception 
    异常是指一条指令会引起 CPU 的不快,比如除零。有群众说了,如果我除零错了,操作系统把我终止了不就完了,我回去改程序,改对了重新运行不就行了么。 
     
    但是有时候 CPU 希望操作系统能够排除这个异常,然后 CPU 重新尝试去执行这条引起异常的指令。这有什么用呢?下面我给大家介绍一个十分重要的异常,缺页异常。 
     
    大家都知道,现代的 CPU 都支持虚拟内存管理,我们还是在我们的虚拟 CPU 上讨论这个问题,上面我们说过了,我们的 CPU 使用 2 级页表映射,叶面大小 4K。我实在懒得写如何映射了,请大家参考 Intel 的手册。因为我们的重点不在这里。看下面的语句: 
     
     char *p = (char *)malloc(100 * 1024 * 1024); 
     
    有人说,没什么不同啊,只不过申请的内存稍微有点儿多啊。但操作系统真地给你那么多内存了么?如果这样的程序来上几个,系统内存岂不是早被耗光,但实际上并没有。所以操作系统采用了在我国盛行的一种机制:打白条!其实我们申请内存的时候操作系统仅仅在虚空间中分配了内存,也就是说仅仅是标记着,这100M的内存归你用,但是我先不给你,当你真的用的时候我再给你分配,这个分配指的就是实实在在的物理页面了。具体怎么实现的呢?我们看下面的语句发生了什么? 
     
     p[0x4538] = 'A'; 
     
    有人疑问了,普通的赋值语句啊。没错,但是这条赋值语句执行了两次(这可不一定啊,我没说绝对,我只是在介绍一种机制),第一次没成功,因为发生了缺页异常,我们刚才说了操作系统仅仅是把这 100M 内存分配给用户了,但是没有对应真正的物理页面。操作系统并没有为 p+0x4538 所在的页面建立页表映射。所以缺页异常发生了。然后操作系统一看这个地址是已经分配给你了,我给你找个物理页面,给你建立好映射,你再执行一次试试。就这一点来说,操作系统比我们的某些官老爷信誉要良好的多,白条兑现了。 
     
    于是第二次执行成功了。有人看到这里已经满头雾水了,这个老家伙到底想说什么? 
     
    注意到了么,操作系统要给他临时找一个页面,找不到怎么办?对,页面交换,找个倒霉蛋,把它的一部分页面写到硬盘上,实际上操作系统只要空闲物理页面少于一定的程度就会做 swap。那么,如果你有个程序需要较高的效率,较好的反应速度,算法写得再好也没用,一个页面被交换出去全完。 
     
    现在明白了吧,优化程序,了解操作系统的运行机制是必不可少的。当然了优化程序绝不仅仅是这些。所以一个优秀的程序员十分有必要知道,你的程序到底运行在 “什么” 上面。 
     
    稍微总结一下: 
    陷阱:由 trap 指令引起,恢复后 CPU 执行下一条指令 
    中断:由硬件电平引起,恢复后 CPU 执行下一条指令 
    异常:由软件指令引起,恢复后 CPU 重新执行该条指令 
     
    有个牛人说过,Oracle 的数据库为什么总比别人的快一点点呢?因为那批人是写操作系统的��
囫囵C语言(三):谁调用了我的 main? 
     
    现在最重要的是要跟得上潮流,所以套用比较时髦的话,谁动了我的奶酪。谁调用了我的 main?不过作为计算机工作者,我劝大家还是不要赶时髦,今天Java热,明天 .net 流行,什么时髦就学什么。我的意思是先花几年把基本功学好,等你赶时髦的时候也好事半功倍。废话不多说了。 
     
    我们都听说过一句话:“main是C语言的入口”。我至今不明白为什么这么说。就好像如果有人说:“挣钱是泡妞”,肯定无数砖头拍过来。这句话应该是“挣钱是泡妞的一个条件,只不过这个条件特别重要”。那么上面那句话应该是 “main是C语言中一个符号,只不过这个符号比较特别。” 
     
    我们看下面的例子: 
     
    /* file name test00.c */ 
     
    int main(int argc, char* argv) 
    { 
     return 0; 
    } 
     
    编译链接它: 
    cc test00.c -o test.exe 
    会生成 test.exe 
     
    但是我们加上这个选项: -nostdlib (不链接标准库) 
    cc test00.c -nostdlib -o test.exe 
    链接器会报错: 
    undefined symbol: __start 
     
    也就是说: 
    1. 编译器缺省是找 __start 符号,而不是 main 
    2. __start 这个符号是程序的起始点 
    3. main 是被标准库调用的一个符号 
     
    再来思考一个问题: 
    我们写程序,比如一个模块,通常要有 initialize 和 de-initialize,但是我们写 C 程序的时候为什么有些模块没有这两个过程么呢?比如我们程序从 main 开始就可以 malloc,free,但是我们在 main 里面却没有初始化堆。再比如在 main 里面可以直接 printf,可是我们并没有打开标准输出文件啊。(不知道什么是 stdin,stdout,stderr 以及 printf 和 stdout 关系的群众请先看看 C 语言中文件的概念)。 
     
    有人说,这些东西不需要初始化。如果您真得这么想,请您不要再往下看了,我个人认为计算机软件不适合您。 
     
    聪明的人民群众会想,一定是在 main 之前干了些什么。使这些函数可以直接调用而不用初始化。通常,我们会在编译器的环境中找到一个名字类似于 crt0.o 的文件,这个文件中包含了我们刚才所说的 __start 符号。(crt 大概是 C Runtime 的缩写,请大家帮助确认一下。) 
     
    那么真正的 crt0.s 是什么样子呢?下面我们给出部分伪代码: 
     
    /////////////////////////////////////////////////////// 
    section .text: 
    __start: 
     
     : 
     init stack; 
     init heap; 
     open stdin; 
     open stdout; 
     open stderr; 
     : 
     push argv; 
     push argc; 
     call _main; (调用 main) 
     : 
     destory heap; 
     close stdin; 
     close stdout; 
     close stderr; 
     : 
     call __exit; 
    //////////////////////////////////////////////////// 
     
    实际上可能还有很多初始化工作,因为都是和操作系统相关的,笔者就不一一列出了。 
     
    注意: 
    1. 不同的编译器,不一定缺省得符号都是 __start。 
    2. 汇编里面的 _main 就是 C 语言里面的 main,是因为汇编器和C编译器对符号的命名有差异(通常是差一个下划线'_')。 
    3. 目前操作系统结构有两个主要的分支:微内核和宏内核。微内核的优点是,结构清晰,简单,内核组件较少,便于维护;缺点是,进程间通信较多,程序频繁进出内核,效率较低。宏内核正好相反。我说这个是什么目的是:没办法保证每个组件都在用户空间(标准库函数)中初始化,有些组件确实可能不要初始化,操作系统在创建进程的时候在内核空间做的。这依赖于操作系统的具体实现,比如堆,宏内核结构可能在内核初始化,微内核结构在用户空间;即使同样是微内核,这个东东也可能会被拿到内核空间初始化。 
     
    随着 CPU 技术的发展,存储量的迅速扩展,代码复杂程度的增加,微内核被越来越多的采用。你会为了 10% 的效率使代码复杂度增加么?要知道每隔 18 个月 CPU 的速度就会翻一番。所以我对程序员的要求是,我首先不要你的代码效率高,我首先要你的代码能让 80% 的人迅速看懂并可以维护��
							 
							 
							 
							  
							  
							  楼主 2016-01-28 14:29 回复
						 
						 
           
          
          
         
   
         
      
 
   
             
                  
                  
 
 
 
     
	 
  
	Copyright © 2010~2015 直线网 版权所有,All Rights Reserved.沪ICP备10039589号
	
	意见反馈 | 
	关于直线 | 
	版权声明 | 
	会员须知