hao对《Linux C编程一站式学习》的笔记(13)
-
第26页 程序的基本概念
自然语言和形式语言
自然语言(Natural Language)就是人类讲的语言。形式语言(Formal Language)是为了特定应用而人为设计的语言。例如数学家用的数字和运算符号、化学家用的分子式等。编程语言也是一种形式语言,是专门设计用来表达计算过程的形式语言。 形式语言有严格的语法(Syntax)规则,。语法规则是由关于符号(Token)和结构(Structure)的规则所组成的。Token的概念相当于自然语言中的单词和标点、数学式中的数和运算符、化学分子式中的元素名和数字。语法规则的第二个范畴是结构,也就是Token的排列方式。关于Token的规则称为词法(Lexical)规则,而关于语句结构的规则称为语法(Grammar)规则 当阅读一个自然语言的句子或者一种形式语言的语句时,你不仅要搞清楚每个词(或Token)是什么意思,而且必须搞清楚整个句子的结构是什么样的(在自然语言中你只是没有意识到,但确实这样做了,尤其是在读外语时你肯定也意识到了)。这个分析句子结构的过程称为解析(Parse)。 虽然形式语言和自然语言有很多共同之处,包括Token、结构和语义,但是也有很多不一样的地方。 歧义性(Ambiguity) 自然语言充满歧义,人们通过上下文的线索和其它一些信息来解决这个问题。形式语言的设计要求是清晰的、毫无歧义的,这意味着每一个语句必须有确切的含义而不管上下文如何。 冗余性(Redundancy) 为了消除歧义减少误解,自然语言引入了相当多的冗余。结果是自然语言经常变得啰里啰嗦,而形式语言则更加紧凑,极少有冗余。 与字面意思的一致性 自然语言充斥着成语和隐喻(Metaphor),我在某种场合下说“The other shoe fell”,可能并不是说谁的鞋掉了。而形式语言中字面(Literal)意思基本上就是真实意思,也有些特殊情况,例如C语言的转义序列(Escape Sequence),但也都会明确规定哪些字面意思不是真实意思,它们所表示的真实意思又是什么。 说自然语言长大的人(实际上没有人例外),往往有一个适应形式语言的困难过程。某种意义上,形式语言和自然语言之间的不同正像诗歌和说明文的区别,当然,前者的区别比后者更明显: 诗歌 词语的发音和意思一样重要,全诗作为一个整体创造出一种效果或者表达出一种感情。歧义和非字面意思不仅是常见的而且是刻意使用的。 说明文 词语的字面意思显得更重要,而且结构能传达出更多的信息。诗歌只能看一个整体,而说明文更适合逐字句分析,但仍然充满歧义。 程序 计算机程序是毫无歧义的,字面和本意高度一致的,能够完全通过对Token和结构的分析加以理解。 引自 程序的基本概念 程序的调试
编程是一个复杂的过程,因为是人做的事情,所以难免经常出错。据说有这样一个典故:早期的计算机体积都很大,有一次一台计算机不能正常工作了,工程师们找了半天原因最后发现是一只臭虫钻进计算机中造成的。从此以后,程序中的错误被叫做臭虫(Bug),而找到这些Bug并加以纠正的过程就叫做调试(Debug)。有时候调试是一件非常复杂的工作,要求程序员概念明确、逻辑清晰、性格沉稳,还需要一点运气。调试的技能我们在后续的学习中慢慢培养,但首先我们要区分清楚程序中的Bug分为哪几类。 编译时错误 编译器只能翻译语法正确的程序,否则将导致编译失败,无法产生目标代码。对于自然语言来说,一点语法错误不是很严重的问题,因为我们仍然可以读懂句子。可惜编译器就没那么宽容了,只要有哪怕一个很小的语法错误,编译器就会输出一条错误提示信息然后罢工,你就得不到你想要的目标代码。虽然大部分情况下编译器给出的错误提示信息就是你出错的代码行,但也有个别时候编译器给出的错误提示信息帮助不大,甚至会误导你。在开始学习编程的前几个星期,你可能会花大量的时间来纠正语法错误。等到有了一些经验之后,还是会犯这样的错误,不过会少得多,而且你能更快地发现错误原因。等到经验更丰富之后你就会觉得,语法错误是最简单最低级的错误,编译器的错误提示也就那么几种,即使错误提示是误导的,也能够立刻找出错误原因是什么。相比下面两种错误,语法错误解决起来要容易得多。 运行时错误 编译器检查不出这类错误,仍然可以生成目标代码,但在运行时会出错而导致程序崩溃。对于我们接下来的几章将编写的简单程序来说,运行时错误很少见,不过到了后面的章节你可能会开始遇到很多运行时错误,例如每个初学者都会遇到的段错误(SegmentationFault)。希望读者在以后的学习中时刻注意区分编译时和运行时(Run-time)这两个概念,不仅是调试,在掌握C语言的很多特性时都需要区分这两个概念,有些事情在编译时做,有些事情则在运行时做。 逻辑错误和语义错误 第三类错误是逻辑错误和语义错误。如果程序里有逻辑错误,编译和运行都会很顺利,看上去也不产生任何错误信息,但是程序没有干它该干的事情,而是干了些别的什么。当然不管怎么样,计算机只会按你写的程序去做,问题出在你写的程序不是你真正想要的,这意味着程序的意思(即语义)是错的。找到逻辑错误在哪需要十分清醒的头脑,因为要通过观察程序的输出而回过头来判断它到底在做什么。 引自 程序的基本概念 -
第34页 常量、变量和表达式
关于C标准
C语言的发展历史大致上分为三个阶段:Old StyleC、C89和C99。Ken Thompson和Dennis Ritchie发明C语言时有很多语法和现在并不一样,但为了向后兼容性(BackwardCompatibility),这些语法仍然在C89和C99中保留下来了,本书不使用Old Style C,但在必要的地方会加以说明。C89是最早的C语言规范,于1989年提出,1990年先由ANSI(美国国家标准委员会,American National Standards Institute)推出ANSI版本,后来被接纳为ISO国际标准(ISO/IEC 9899:1990),因而有时也称为C90,最经典的C语言教材[K&R]就是基于这个版本的,C89是目前最广泛采用的C语言标准,大多数编译器都完全支持C89。C99标准(ISO/IEC 9899:1999)是在1999年推出的,加入了许多新的特性,但目前仍没有得到广泛支持,在C99推出之后相当长的一段时间里,连gcc也没有完全实现C99的所有特性。C99标准详见[C99]。本书内容以C89为主,在必要的地方会说明一下C99的新特性,但是不建议使用。 C标准的目的是为了精确定义C语言,而不是为了教别人怎么编程,C标准在表达上追求准确和无歧义,却十分不容易看懂,[Standard C]和[Standard C Library]是对C89及其修订版本的阐释(可惜作者没有随C99更新),比C标准更容易看懂,另外,参考[C99 Rationale]也有助于加深对C标准的理解。 C标准规定的转义字符 \'单引号'(Single Quote,或Apostrophe) \"双引号" \?问号?(Question Mark) \\反斜线\(Backslash) \a响铃(Alert,或Bell) \b退格(Backspace) \f分页符(Form Feed) \n换行(Line Feed) \r回车(Carriage Return) \t水平制表符(Horizontal Tab) \v垂直制表符(Vertical Tab) 引自 常量、变量和表达式 -
第103页 深入理解函数
递归
一个重要结论:递归和循环是等价的,用循环能做的事用递归都能做,反之亦然,事实上有的编程语言(如某些LISP)只有递归而没有循环。计算机硬件能做的所有事情就是数据存取、运算、测试和分支、循环(或递归),在计算机上运行的高级语言写的程序当然也不可能做到更多的事情,虽然高级语言有丰富的语法特性,但也只是为做这些事情提供一些方便。 引自 深入理解函数 抽象
数据抽象-------比如typedef, struct 过程抽象--------把一组语句封装成函数 抽象层(Abstraction Layer),从底层往上层来看,“复数”这种数据越来越抽象了,把所有这些层组合在一起就是一个完整的系统。组合使得系统可以任意复杂,而抽象使得系统的复杂性是可以控制的,任何改动都只局限在某一层,而不会波及整个系统。著名的计算机科学家Butler Lampson说过:“All problems in computerscience can be solved by another level of indirection.”这里的indirection其实就是abstraction的意思。 引自 深入理解函数 -
第133页 编码风格
函数
每个函数都应该设计得尽可能简单,简单的函数才容易维护。应遵循以下原则: 1. 实现一个函数只是为了做好一件事情,不要把函数设计成用途广泛、面面俱到的,这样的函数肯定会超长,而且往往不可重用,维护困难。 2. 函数内部的缩进层次不宜过多,一般以少于4层为宜。如果缩进层次太多就说明设计得太复杂了,应该考虑分割成更小的函数来调用(这称为Helper Function)。 3. 函数不要写得太长,建议在24行的标准终端上不超过两屏,太长会造成阅读困难,如果一个函数超过两屏就应该考虑分割函数了。[ CodingStyle]中特别说明,如果一个函数在概念上是简单的,只是长度很长,这倒没关系。例如函数由一个大的switch组成,其中有非常多的case,这是可以的,因为各个case之间互不影响,整个函数的复杂度只等于其中一个case的复杂度,这种情况很常见,例如TCP协议的状态机实现。 4. 执行函数就是执行一个动作,函数名通常应包含动词,例如get_current、radix_tree_insert。 5. 比较重要的函数定义上面必须加注释,说此函数的功能、参数、返回值、错误码等。 6. 另一种度量函数复杂度的办法是看有多少个局部变量,5到10个局部变量就已经很多了,局部变量再多就很难维护了,应该考虑分割函数。 引自 编码风格 (indent工具可以把代码格式化为某种风格)
-
第156页 排序与查找
插入排序
#include <stdio.h> #define LEN 5 int a[LEN] = { 10, 5, 2, 4, 7 }; void insertion_sort(void) { int i, j, key; for (j = 1; j < LEN; ++j) { printf("%d, %d, %d, %d, %d\n", a[0], a[1], a[2], a[3], a[4]); key = a[j]; i = j - 1; while (i >= 0 && a[i] > key) { a[i+1] = a[i]; --i; } a[i+1] = key; } printf("%d, %d, %d, %d, %d\n", a[0], a[1], a[2], a[3], a[4]); } int main(void) { insertion_sort(); return 0; }
归并排序
#include <stdio.h> #define LEN 8 int a[LEN] = { 5, 2, 4, 7, 1, 3, 2, 6 }; void merge(int start, int mid, int end) { int n1 = mid - start + 1; int n2 = end - mid; int left[n1], right[n2]; int i, j, k; for (i = 0; i < n1; i++) /* left holds a[start..mid] */ left[i] = a[start+i]; for (j = 0; j < n2; ++j) /* right holds a[mid+1..end] */ right[j] = a[mid+1+j]; i = j = 0; for (k = start; i < n1 && j < n2; ++k) { if (left[i] < right[j]) { a[k] = left[i]; ++i; } else { a[k] = right[j]; ++j; } } if (i < n1) /* left[] is not exhausted */ for (; i < n1; i++) { a[k] = left[i]; ++k; } if (j < n2) /* right[] is not exhausted */ for (; j < n2; ++j) { a[k] = right[j]; ++k; } } void sort(int start, int end) { int mid; if (start < end) { mid = (start + end) / 2; printf("sort (%d-%d, %d-%d) %d %d %d %d %d %d %d %d\n", start, mid, mid+1, end, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]); sort(start, mid); sort(mid + 1, end); merge(start, mid, end); printf("merge (%d-%d, %d-%d) to %d %d %d %d %d %d %d %d\n", start, mid, mid+1, end, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]); } } int main(void) { sort(0, LEN-1); return 0; }
-
第177页 栈与队列
深度优先(DFS)
定义一个二维数组maze--------它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的路线。 引自 栈与队列 栈与深度优先搜索
#include <stdio.h> #define MAX_ROW 5 #define MAX_COL 5 struct point { int row, col; } stack[512]; int top = 0; void push(struct point p) { stack[top] = p; top++; } struct point pop(void) { top--; return stack[top]; } int is_empty(void) { return top == 0; } int maze[MAX_ROW][MAX_COL] = { 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, }; void print_maze(void) { int i, j; for (i = 0; i < MAX_ROW; i++) { for (j = 0; j < MAX_COL; j++) printf("%d ", maze[i][j]); putchar('\n'); } printf("*********\n"); } struct point predecessor[MAX_ROW][MAX_COL] = { {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}}, {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}}, {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}}, {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}}, {{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}}, }; void visit(int row, int col, struct point pre) { struct point visit_point = { row, col }; maze[row][col] = 2; predecessor[row][col] = pre; push(visit_point); } int main(void) { struct point p = { 0, 0 }; maze[p.row][p.col] = 2; push(p); while (!is_empty()) { p = pop(); if (p.row == MAX_ROW - 1 /* goal */ && p.col == MAX_COL - 1) break; if (p.col+1 < MAX_COL /* right */ && maze[p.row][p.col+1] == 0) visit(p.row, p.col+1, p); if (p.row+1 < MAX_ROW /* down */ && maze[p.row+1][p.col] == 0) visit(p.row+1, p.col, p); if (p.col-1 >= 0 /* left */ && maze[p.row][p.col-1] == 0) visit(p.row, p.col-1, p); if (p.row-1 >= 0 /* up */ && maze[p.row-1][p.col] == 0) visit(p.row-1, p.col, p); print_maze(); } if (p.row == MAX_ROW - 1 && p.col == MAX_COL - 1) { printf("(%d, %d)\n", p.row, p.col); while (predecessor[p.row][p.col].row != -1) { p = predecessor[p.row][p.col]; printf("(%d, %d)\n", p.row, p.col); } } else printf("No path!\n"); return 0; }
队列与广度优先搜索
#include <stdio.h> #define MAX_ROW 5 #define MAX_COL 5 struct point { int row, col, predecessor; } queue[512]; int head = 0, tail = 0; void enqueue(struct point p) { queue[tail] = p; tail++; } struct point dequeue(void) { head++; return queue[head-1]; } int is_empty(void) { return head == tail; } int maze[MAX_ROW][MAX_COL] = { 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, }; void print_maze(void) { int i, j; for (i = 0; i < MAX_ROW; i++) { for (j = 0; j < MAX_COL; j++) printf("%d ", maze[i][j]); putchar('\n'); } printf("*********\n"); } void visit(int row, int col) { struct point visit_point = { row, col, head-1}; maze[row][col] = 2; enqueue(visit_point); } int main(void) { struct point p = { 0, 0, -1 }; maze[p.row][p.col] = 2; enqueue(p); while (!is_empty()) { p = dequeue(); if (p.row == MAX_ROW - 1 /* goal */ && p.col == MAX_COL - 1) break; if (p.col+1 < MAX_COL /* right */ && maze[p.row][p.col+1] == 0) visit(p.row, p.col+1); if (p.row+1 < MAX_ROW /* down */ && maze[p.row+1][p.col] == 0) visit(p.row+1, p.col); if (p.col-1 >= 0 /* left */ && maze[p.row][p.col-1] == 0) visit(p.row, p.col-1); if (p.row-1 >= 0 /* up */ && maze[p.row-1][p.col] == 0) visit(p.row-1, p.col); print_maze(); } if (p.row == MAX_ROW - 1 && p.col == MAX_COL - 1) { printf("(%d, %d)\n", p.row, p.col); while (p.predecessor != -1) { p = queue[p.predecessor]; printf("(%d, %d)\n", p.row, p.col); } } else printf("No path!\n"); return 0; }
-
第200页 数据类型详解
整型
整数常量还可以在末尾在加u或U表示“unsigned”,加l或L表示“long”,加ll或LL表示“longlong” 引自 数据类型详解 Implementation-defined、Unspecified和Undefined 在C标准中没有做明确规定的地方会用Implementation-defined、Unspecified或Undefined来表述,在本书中有时把这三种情况统称为“未明确定义”的。这三种情况到底有什么不同呢? 我们刚才看到一种Implementation-defined的情况,C标准没有明确规定char是有符号的还是无符号的,但是要求编译器必须对此做出明确规定,并写在编译器的文档中。 而对于Unspecified的情况,往往有几种可选的处理方式,C标准没有明确规定按哪种方式处理,编译器可以自己决定,并且也不必写在编译器的文档中,这样即使用同一个编译器的不同版本来编译也可能得到不同的结果,因为编译器没有在文档中明确写它会怎么处理,那么不同版本的编译器就可以选择不同的处理方式,比如下一章我们会讲到一个函数调用的各个实参表达式按什么顺序求值是Unspecified的。 Undefined的情况则是完全不确定的,C标准没规定怎么处理,编译器很可能也没规定,甚至也没做出错处理,有很多Undefined的情况是编译器是检查不出来的,最终会导致运行时错误,比如数组访问越界就是Undefined的。 初学者看到这些规则通常会很不舒服,觉得这不是在学编程而是在啃法律条文,结果越学越泄气。是的,C语言并不像一个数学定理那样完美,现实世界里的东西总是不够完美的。但还好啦,C程序员已经很幸福了,只要严格遵照C标准来写代码,不要去触碰那些阴暗角落,写出来的代码就有很好的可移植性。想想那些可怜的JavaScript程序员吧,他们甚至连一个可以遵照的标准都没有,一个浏览器一个样,因而不得不为每一种浏览器的每一个版本分别写不同的代码。 引自 数据类型详解 浮点型
C标准规定的浮点型有float、double、long double,和整数类型一样,既没有规定每种类型占多少字节,也没有规定采用哪种表示形式。浮点数的实现在各种平台上差异很大,有的处理器有浮点运算单元(称为硬件实现),有的处理器没有,只能做整数运算,那么就要用整数运算来模拟浮点运算(称为软件实现),虽然大部分平台的浮点数实现(硬件或软件实现)是遵循IEEE 754的,但仍有很多平台的实现没有遵循IEEE 754。x86处理器通常是有浮点运算单元的,遵循IEEE 754,float型通常是32位,double型通常是64位。 引自 数据类型详解 Integer Promotion
在一个表达式中,凡是可以使用int或unsigned int类型做右值的地方也都可以使用有符号或无符号的char型、short型和Bit-field。如果原始类型的取值范围都能用int型表示,则其值被提升为int型,如果表示不了就提升为unsigned int型,这称为Integer Promotion。做IntegerPromotion只影响上述几种类型的值,对其它类型无影响。 引自 数据类型详解 -
第230页 运算符详解
逗号运算符
逗号运算符(Comma Operator)也是一种双目运算符,它的形式是表达式1, 表达式2,两个表达式不要求类型一致,左边的表达式1先求值,求完了直接把值丢掉,再求右边表达式2的值作为整个表达式的值。逗号运算符是左结合的,类似于+-*/运算符,根据组合规则可以写出表达式1,表达式2, 表达式3, ..., 表达式n这种形式,表达式1, 表达式2可以看作一个子表达式,先求表达式1的值,然后求表达式2的值作为这个子表达式的值,然后这个值再和表达式3组成一个更大的表达式,求表达式3的值作为这个更大的表达式的值,依此类推,整个计算过程就是从左到右依次求值,最后一个表达式的值成为整个表达式的值。 引自 运算符详解 sizeof运算符与typedef类型声明
sizeof表达式的值是size_t类型的,这个类型定义在stddef.h头文件中,不过你的代码中只要不出现size_t这个类型名就不用包含这个头文件,比如像上面的例子就不用包含这个头文件。size_t这个类型是我们讲过的整型中的某一种,编译器可能会用typedef做一个类型声明:typedef unsigned long size_t; 那么size_t类型就是unsigned long类型。之所以不直接规定sizeof的值是unsigned long型的,而要规定一个size_t类型,是为了允许不同的编译器根据自己平台的情况定义size_t为不同的类型,这样使用size_t类型的代码就具有很好的可移植性,但不管编译器怎么实现,C标准明确规定sizeof的值是无符号整型的。 引自 运算符详解 Side Effect与Sequence Point 举例,某种傻逼题:
int a=0; a = (++a)+(++a)+(++a)+(++a); 据我了解,似乎很多公司都有出这种笔试题的恶趣味。答案应该是Undefined,我甚至有些怀疑出题的人是否真的知道答案。下面我来解释为什么是Undefined。 我们知道,调用一个函数可能产生Side Effect,使用某些运算符(++、--、=、复合赋值)也会产生Side Effect,如果一个表达式中隐含着多个Side Effect,究竟哪个先发生哪个后发生呢?C标准规定代码执行过程中的某些时刻是Sequence Point,当到达一个SequencePoint时,在此之前的Side Effect必须全部作用完毕,在此之后的Side Effect必须一个都没发生。至于两个Sequence Point之间的多个Side Effect哪个先发生哪个后发生则没有规定,编译器可以任意选择各Side Effect的作用顺序。下面详细解释各种Sequence Point(出自[C99]的Annex C) 1、调用一个函数时,在所有准备工作做完之后、函数调用开始之前是Sequence Point。比如调用foo(f(), g())时,foo、f()、g()这三个表达式哪个先求值哪个后求值是Unspecified,但是必须都求值完了才能做最后的函数调用,所以f()和g()的Side Effect按什么顺序发生不一定,但必定在这些Side Effect全部作用完之后才开始调用foo函数。 2、条件表达式?:、逗号运算符,、逻辑与&&、逻辑或||的第一个操作数求值之后是SequencePoint。我们刚讲过条件表达式和逗号运算符,条件表达式要根据表达式1的值是否为真决定下一步求表达式2还是表达式3的值,如果决定求表达式2的值,表达式3就不会被求值了,反之也一样,逗号运算符也是这样,表达式1求值结束才继续求表达式2的值。 引自 运算符详解 -
第243页 计算机体系结构基础
设备
有些设备像内存芯片一样连接到处理器的地址总线和数据总线,正因为地址线和数据线上可以挂多个设备和内存芯片所以才叫“总线”,但不同的设备和内存应该占不同的地址范围。访问这种设备就像访问内存一样,按地址读写即可,和访问内存不同的是,往一个地址写数据只是给设备发一个命令,数据不一定要保存,从一个地址读出的数据也不一定是先前保存在这个地址的数据,而是设备的某个状态。设备中可供读写访问的单元通常称为设备寄存器(注意和CPU的寄存器不是一回事),操作设备的过程就是对这些设备寄存器做读写操作的过程,比如向串口发送寄存器里写数据,串口设备就会把数据发送出去,读串口接收寄存器的值,就可以读取串口设备接收到的数据。 还有一些设备是集成在处理器芯片中。在上图中,从CPU核引出的地址和数据总线有一端经总线接口引出到芯片引脚上了,还有一端没有引出,而是接到芯片内部集成的设备上,这些设备都有各自的内存地址范围,也可以像访问内存一样访问,很多体系结构(比如ARM)采用这种方式操作设备,称为内存映射I/O(Memory-mapped I/O)。但是x86比较特殊,x86对于设备有独立的端口地址空间,CPU核需要引出额外的地址线来连接片内设备,访问设备寄存器时用特殊的in/out指令,而不是和访问内存用同样的指令,这种方式称为端口I/O(Port I/O)。 从CPU的角度来看,访问设备只有内存映射I/O和端口I/O两种,要么像内存一样访问,要么用一种专用的指令访问。其实访问设备是相当复杂的,由于计算机的设备五花八门,各种设备的性能要求都不一样,有的要求带宽大,有的要求响应快,有的要求热插拔,于是出现了各种适应不同要求的设备总线,比如PCI、AGP、USB、1394、SATA等等,这些设备总线并不直接和CPU相连,CPU通过内存映射I/O或端口I/O访问相应的总线控制器,通过它再去访问挂在总线上的设备。 访问设备还有一点和访问内存不同。内存只是保存数据而不会产生新的数据,如果CPU不去读它,它也不需要主动提供数据给CPU,所以内存总是被动地等待被读或被写。而设备往往会自己产生数据,并且需要主动通知CPU来读这些数据,例如敲键盘产生一个输入字符,用户希望计算机马上响应自己的输入,这就要求键盘设备主动通知CPU来读这个字符并做相应处理,给用户响应。这是由中断(Interrupt)机制实现的,每个设备都有一条中断线,通过中断控制器连接到CPU,当设备需要主动通知CPU时就引发一个中断信号,CPU正在执行的指令将被打断,程序计数器会设置成某个固定的地址(这个地址由体系结构定义),于是CPU从这个地址开始取指令(或者说跳转到这个地址),执行中断服务程序(ISR,Interrupt Service Routine),完成中断处理之后再返回先前被打断的地方执行后续指令。比如某种体系结构规定发生中断时跳转到地址0x0000 0010执行,那么就要事先把一段ISR程序加载到这个地址,ISR程序是由内核代码提供的,中断处理的步骤通常是先判断哪个设备引发了中断,然后调用该设备驱动程序提供的中断处理函数(Interrupt Handler)做进一步处理。 由于各种设备的用途各不相同,设备寄存器中每个位的定义和操作方法也各不相同,所以每种设备都需要专门的设备驱动程序(Device Driver),一个操作系统为了支持广泛的设备就需要有大量的设备驱动程序,事实上,Linux内核源代码中绝大部分是设备驱动程序。设备驱动程序通常是操作系统内核里的一组函数,主要是通过对设备寄存器的读写实现对设备的初始化、读、写等操作,有些设备还要提供一个中断处理函数供ISR调用。 引自 计算机体系结构基础 MMU
现代操作系统普遍采用虚拟内存管理(Virtual Memory Management)机制,这需要MMU(Memory Management Unit,内存管理单元)的支持。有些嵌入式处理器没有MMU,则不能运行依赖于虚拟内存管理的操作系统。 首先引入两个概念,虚拟地址和物理地址。如果处理器没有MMU,或者有MMU但没有启用,CPU执行单元发出的内存地址将直接传到芯片引脚上,被内存芯片(以下称为物理内存,以便与虚拟内存区分)接收,这称为物理地址(Physical Address,以下简称PA) 如果处理器启用了MMU,CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址 事实上,在启用MMU的情况下虚拟地址空间和物理地址空间是完全独立的,物理地址空间既可以小于也可以大于虚拟地址空间 MMU将虚拟地址映射到物理地址是以页(Page)为单位的,对于32位CPU通常一页为4KB。例如,MMU可以通过一个映射项将虚拟地址的一页0xb7001000~0xb7001fff映射到物理地址的一页0x2000~0x2fff,物理内存中的页称为物理页面或页帧(Page Frame)。至于虚拟内存的哪个页面映射到物理内存的哪个页帧,这是通过页表(Page Table)来描述的,页表保存在物理内存中,MMU会查找页表来确定一个虚拟地址应该映射到什么物理地址。总结一下这个过程: 1. 在操作系统初始化或者分配、释放内存时,会执行一些指令在物理内存中填写页表,然后用指令设置MMU,告诉MMU页表在物理内存中的什么位置。 2. 设置好之后,CPU每次执行访问内存的指令都会自动引发MMU做查表和地址转换的操作,地址转换操作完全由硬件完成,不需要用指令控制MMU去做。 MMU除了做地址转换之外,还提供内存保护机制。各种体系结构都有用户模式(UserMode)和特权模式(Privileged Mode)之分,操作系统可以设定每个内存页面的访问权限,有些页面不允许访问,有些页面只有在CPU处于特权模式时才允许访问,有些页面在用户模式和特权模式都可以访问,允许访问的权限又分为可读、可写和可执行三种。这样设定好之后,当CPU要访问一个VA时,MMU会检查CPU当前处于用户模式还是特权模式,访问内存的目的是读数据、写数据还是取指令,如果和操作系统设定的页面权限相符,就允许访问,把它转换成PA,否则不允许访问,产生一个异常(Exception)。 引自 计算机体系结构基础 -
第262页 x86汇编程序基础
ELF文件
ELF文件格式是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,它有三种不同的类型: 可重定位的目标文件(Relocatable) 可执行文件(Executable) 共享库(Shared Object) 引自 x86汇编程序基础 目标文件
ELF文件格式提供了两种不同的视角,在汇编器和链接器看来,ELF文件是由Section HeaderTable描述的一系列Section的集合,而执行一个ELF文件时,在加载器(Loader)看来它是由Program Header Table描述的一系列Segment的集合。 开头的ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在汇编和链接过程中没有用到,所以是可有可无的,SectionHeader Table中保存了所有Section的描述信息。右边是从加载器的视角来看这个文件,开头是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section HeaderTable在加载过程中没有用到,所以是可有可无的。 引自 x86汇编程序基础
hao的其他笔记 · · · · · · ( 全部427条 )
- P2P网络技术原理与C++开发案例
- 5
- 大话移动通信
- 1
- 图解网络硬件
- 1
- 写给大家看的C++书
- 1
- Orange'S
- 1
- 虚拟机
- 1
- 大规模C++程序设计
- 10
- Linux Shell脚本攻略
- 3
- HTTP权威指南
- 3
- C++ API设计
- 8
- C++语言的设计和演化
- 5
- 构建嵌入式LINUX系统
- 3
- 七周七语言
- 15
- 链接器和加载器
- 1
- Windows网络与通信程序设计
- 2
- Windows核心编程(第5版)
- 16
- C++GUI Qt4编程
- 3
- 图解TCP/IP (第5版)
- 12
- Python学习手册
- 5
- 编写可读代码的艺术
- 14
- 你一定爱读的极简欧洲史
- 2
- 短码之美
- 1
- 程序员的自我修养
- 17
- 美国纽约摄影学院摄影教材(上)
- 2
- Win32多线程程序设计
- 8
- 竹林蹊径
- 4
- 鸟哥的Linux私房菜
- 4
- Linux/Unix设计思想
- 6
- 程序设计语言的形式语义
- 1
- Windows驱动开发技术详解
- 9
- 你必须知道的495个C语言问题
- 5
- Windows内核原理与实现
- 44
- PCI Express 体系结构导读
- 3
- 程序员的数学
- 5
- 深入浅出 MFC 第二版
- 2
- 并发的艺术
- 4
- 群和它的图象表示
- 1
- C语言接口与实现
- 1
- 致命元素
- 2
- 深入解析Windows操作系统
- 51
- 淘宝技术这十年
- 1
- 高效程序的奥秘
- 1
- 花田半亩
- 1
- 于丹:重温最美古诗词
- 1
- 诛仙8(大结局)
- 1
- 深入理解计算机系统(原书第2版)
- 9
- C和指针
- 9
- 寒江独钓
- 5
- Windows程序设计
- 9
- 数据结构与算法分析
- 11
- 80X86汇编语言程序设计教程
- 3
- 琢石成器
- 30
- 数学分析教程
- 1
- 操作系统概念(第六版)
- 12
- 算法竞赛入门经典
- 10
- C++反汇编与逆向分析技术揭秘
- 12