中国DOS联盟论坛

中国DOS联盟

-- 联合DOS 推动DOS 发展DOS --

联盟域名:www.cn-dos.net  论坛域名:www.cn-dos.net/forum
DOS,代表着自由开放与发展,我们努力起来,学习FreeDOS和Linux的自由开放与GNU精神,共同创造和发展美好的自由与GNU GPL世界吧!

游客:  注册 | 登录 | 命令行 | 会员 | 搜索 | 上传 | 帮助 »
中国DOS联盟论坛 » 网络日志(Blog) » 1DOS学习中的文章展开,方便阅读。(20160203)
« [1] [2] [3] [4] [5] [6] »
作者:
标题: 1DOS学习中的文章展开,方便阅读。(20160203) 上一主题 | 下一主题
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『楼 主』:  1DOS学习中的文章展开,方便阅读。(20160203)

目录:
1第一个 C 语言编译器是怎样编写的?2楼
2一起来写个简单的解释器                3楼
3手把手教你做一个 C 语言编译器      4楼
4学习较底层编程:动手写一个C语言编译器 5楼
5自制编译器:语法分析器                6楼
6自制编译器:词法分析器                7楼
7cucu: 一个易于理解的编译器          8楼
8 kickout 转贴李家芳的《硬盘分区表详解 》 9楼  (坛内)
9不可思议的物理——与加来道雄对话 10楼
10 LOLI地狱 尼古拉的遗嘱               11楼
11《关于时间》                             12楼
12Linux下的经典软件(史上最全)        13楼




第一页-----------------------------------------------------------------------------------------第一页

如果你进入地狱,请不要慌张,因为你在哪里,哪里就会成为天堂。
害怕,是一条努力不害怕的道路,而勇敢好奇一直在路上陪伴着同行。
(文章全部属于原作者所有,非个人原创,当然个人没有原创,全是转载)
------------------------------------------------------------------------------------------------------------
产品收集:
足球动能发电用于能源不足地区的发明
http://business.sohu.com/20160722/n460421178.shtml
概念人力脚踩发电锻炼

英国Billions in Change 公司推出了一款健身发电车
http://tech.163.com/15/1219/15/BB754ST500094OE0.html

[ Last edited by zzz19760225 on 2018-1-15 at 11:46 ]

2016-2-2 23:09
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 2 楼』:  1第一个 C 语言编译器是怎样编写的?

第一个 C 语言编译器是怎样编写的?
首先向C语言之父Dennis Ritchie致敬!

当今几乎所有的实用的编译器/解释器(以下统称编译器)都是用C语言编写的,有一些语言比如Clojure,Jython等是基于JVM或者说是用Java实现的,IronPython等是基于.NET实现的,但是Java和C#等本身也要依靠C/C++来实现,等于是间接调用了调用了C。所以衡量某种高级语言的可移植性其实就是在讨论ANSI/ISO C的移植性。

C语言是很低级的语言,很多方面都近似于汇编语言,在《Intel32位汇编语言程序设计》一书中,甚至介绍了手工把简单的C语言翻译成汇编的方法。对于编译器这种系统软件,用C语言来编写是很自然不过的,即使是像Python这样的高级语言依然在底层依赖于C语言(举Python的例子是因为因特尔的黑客正在尝试让Python不需要操作系统就能运行——实际上是免去了BIOS上的一次性C代码)。现在的学生,学过编译原理后,只要有点编程能力的都可以实现一个功能简单的类C语言编译器。

可是问题来了,不知道你有没有想过,大家都用C语言或基于C语言的语言来写编译器,那么世界上第一个C语言编译器又是怎么编写的呢?这不是一个“鸡和蛋”的问题……

还是让我们回顾一下C语言历史:1970年Tomphson和Ritchie在BCPL(一种解释型语言)的基础上开发了B语言,1973年又在B语言的基础上成功开发出了现在的C语言。在C语言被用作系统编程语言之前,Tomphson也用过B语言编写操作系统。可见在C语言实现以前,B语言已经可以投入实用了。因此第一个C语言编译器的原型完全可能是用B语言或者混合B语言与PDP汇编语言编写的。我们现在都知道,B语言的效率比较低,但是如果全部用汇编语言来编写,不仅开发周期长、维护难度大,更可怕的是失去了高级程序设计语言必需的移植性。所以早期的C语言编译器就采取了一个取巧的办法:先用汇编语言编写一个C语言的一个子集的编译器,再通过这个子集去递推完成完整的C语言编译器。详细的过程如下:

先创造一个只有C语言最基本功能的子集,记作C0语言,C0语言已经足够简单了,可以直接用汇编语言编写出C0的编译器。依靠C0已有的功能,设计比C0复杂,但仍然不完整的C语言的又一个子集C1语言,其中C0属于C1,C1属于C,用C0开发出C1语言的编译器。在C1的基础上设计C语言的又一个子集C2语言,C2语言比C1复杂,但是仍然不是完整的C语言,开发出C2语言的编译器……如此直到CN,CN已经足够强大了,这时候就足够开发出完整的C语言编译器的实现了。至于这里的N是多少,这取决于你的目标语言(这里是C语言)的复杂程度和程序员的编程能力——简单地说,如果到了某个子集阶段,可以很方便地利用现有功能实现C语言时,那么你就找到N了。下面的图说明了这个抽象过程:

C语言
CN语言
……
C0语言
汇编语言
机器语言
这张图是不是有点熟悉?对了就是在将虚拟机的时候见到过,不过这里是CVM(C Language Virtual Machine),每种语言都是在每个虚拟层上可以独立实现编译的,并且除了C语言外,每一层的输出都将作为下一层的输入(最后一层的输出就是应用程序了),这和滚雪球是一个道理。用手(汇编语言)把一小把雪结合在一起,一点点地滚下去就形成了一个大雪球,这大概就是所谓的0生1,1生C,C生万物吧?

那么这种大胆的子集简化的方法,是怎么实现的,又有什么理论依据呢?先介绍一个概念,“自编译”(Self-Compile),也就是对于某些具有明显自举性质的强类型(所谓强类型就是程序中的每个变量必学声明类型后才能使用,比如C语言,相反有些脚本语言则根本没有类型这一说法)编程语言,可以借助它们的一个有限小子集,通过有限次数的递推来实现对它们自身的表述,这样的语言有C、Pascal、Ada等等,至于为什么可以自编译,可以参见清华大学出版社的《编译原理》,书中实现了一个Pascal的子集的编译器。总之,已经有CS科学家证明了,C语言理论上是可以通过上面说的CVM的方法实现完整的编译器的,那么实际上是怎样做到简化的呢?

下面是C99的关键字:

auto        enum        restrict        unsigned
break       extern      return          void
case        float       short           volatile
char        for         signed          while
const       goto        sizeof          _Bool
continue    if          static          _Complex
default     inline      struct          _Imaginary
do          int         switch        
double      long        typedef
else        register    union
//共37个
仔细看看,其实其中有很多关键字是为了帮助编译器进行优化的,还有一些是用来限定变量、函数的作用域、链接性或者生存周期(函数没有)的,这些在编译器实现的早期根本不必加上,于是可以去掉auto,restrict,extern,volatile,const,sizeof,static,inline,register,typedef,这样就形成了C的子集,C3语言,C3语言的关键字如下:

enum       unsigned
break       return      void
case        float       short   
char        for         signed     while
goto        _Bool
continue    if          _Complex
default     struct      _Imaginary
do          int         switch        
double      long   
else        union
//共27个
再想一想,发现C3中其实有很多类型和类型修饰符是没有必要一次性都加上去的,比如三种整型,只要实现int就行了,因此进一步去掉这些关键词,它们是:unsigned,float,short,char(char is int),signed,_Bool,_Complex,_Imaginary,long,这样就形成了我们的C2语言,C2语言关键字如下:

enum
break      return      void
case
for         while
goto        
continue    if         
default     struct   
do          int         switch        
double  
else        union
//共18个
继续思考,即使是只有18个关键字的C2语言,依然有很多,高级的地方,比如基于基本数据类型的复合数据结构,另外我们的关键字表中是没有写运算符的,在C语言中的复合赋值运算符->运算符等的++,–等过于灵活的表达方式此时也可以完全删除掉,因此可以去掉的关键字有:enum,struct,union,这样我们可以得到C1语言的关键字:

break      return      void
case
for         while
goto        
continue    if         
default  
do          int         switch        
double  
else
//共15个
接近完美了,不过最后一步手笔自然要大一点。这个时候数组和指针也要去掉了,另外C1语言其实仍然有很大的冗杂度,比如控制循环和分支的都有多种表述方法,其实都可简化成一种,具体的来说,循环语句有while循环,do…while循环和for循环,只需要保留while循环就够了;分支语句又有if…{},if…{}…else,if…{}…else if…,switch,这四种形式,它们都可以通过两个以上的if…{}来实现,因此只需要保留if,…{}就够了。可是再一想,所谓的分支和循环不过是条件跳转语句罢了,函数调用语句也不过是一个压栈和跳转语句罢了,因此只需要goto(未限制的goto)。因此大胆去掉所有结构化关键字,连函数也没有,得到的C0语言关键字如下:

break    void
goto        
int     
double  
//共5个
这已经是简约的极致了。

只有5个关键字,已经完全可以用汇编语言快速的实现了。通过逆向分析我们还原了第一个C语言编译器的编写过程,也感受到了前辈科学家们的智慧和勤劳!我们都不过是巨人肩膀上的灰尘罢了!0生1,1生C,C生万物,实在巧妙!

2016-2-2 23:13
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 3 楼』:  2一起来写个简单的解释器

一起来写个简单的解释器
一起来写个简单的解释器(1)
谷歌大牛 Steve Yegge 曾说过:“如果你不知道编译器是怎样工作的,那你也并不知道计算机是怎样工作的。如果你不是 100% 确定你是否知道编译器是怎样工作的,那你其实并不知道它们是怎样工作的。”
这跟你是新手还是经验丰富的软件开发人员无关:如果你不知道编译器和解释器是怎样工作的,那么你就不知道计算机是怎样工作的。就这么简单。

那么,你知道编译器和解释器是怎样工作的吗?我的意思是,你 100% 确定自己知道它们是怎样工作的吗?如果你不知道。



或者说如果你不知道,并且因此而感到不安。



别着急。如果你留下来学习完整个系列,并且和我一起构造一个解释器和编译器,你最终将会知道它们是怎样工作的。并且你将会变成一个自信快乐的人,至少我希望是这样。



为什么要学习解释器和编译器?有三个理由:

1、为了写一个解释器或者一个编译器,你必须综合应用一些技能。编写一个解释器或者编译器,将会帮助你提高这些技能,让你变成一个更优秀的软件开发者。同时这些技术在编写其它软件(非编译器和解释器)时同样很有用。

2.你确实想知道计算机是怎样工作的。常常解释器和编译器看起来像魔术。而你对这种魔术觉得不太舒服。你想弄清楚构造一个解释器和编译器的过程,弄明白它们是怎样工作的,弄明白这里面所有的事。

3.你想创建你自己的语言或者是特定领域的语言。如果你创建了它,那么你同样需要为它创建一个编译器或解释器。最近,人们重新兴起了对新语言的兴趣。你几乎每天都能看新语言的出现:Elixir、Go、Rust,只是随便举几个例子。



好了,那什么是解释器,什么是编译器呢?

解释器和编译器的目标就是将使用高级语言编写的源程序转换成另一种形式。什么形式?稍安勿燥,在本系列的后续部分中,你将会很确切地了解到源程序将被转换成什么。

现在你可能会对解释器和编译器之间有什么区别感到好奇。对于本系列,我们约定,如果一个翻译器将源程序翻译成机器语言,那么它就是一个编译器。如果一个翻译器直接处理并运行源程序,不先把源程序翻译成机器语言,那么它就是一个解释器。直观上它看起来会是这个样子:


我希望此时此刻,你很确信你的愿意学习,并且构建一个解释器和编译器。关于这个解释器系列,你有什么期待呢?

你看这样行不行。我们为 Pascal 语言的一个大子集创建一个简单的解释器。在这个系列的最后,你将得到一个能够工作的解释器以及像 Python 的 pdb 一样的源代码级别的调试器。

那么问题来了,为什么选 Pascal?首先,这不是我为了这个系列捏造的语言:这是一个真实的编程语言,具有许多重要的语言结构。其次,有些计算机书籍虽然旧,但实用,这些书的例子用了 Pascal 语言。(我承认这不是一个选择它来构造解释器的不可抗拒的理由,但我认为这也是一个很好的学习非主流语言的机会:)

这是有一个使用 Pascal 编写的阶乘的例子。你将可以用自己的解释器和调试器来解释和调试这段代码。

program factorial;

function factorial(n: integer): longint;
begin
    if n = 0 then
        factorial := 1
    else
        factorial := n * factorial(n - 1);
end;

var
    n: integer;

begin
    for n := 0 to 16 do
        writeln(n, '! = ', factorial(n));
end.
我们使用 Python 来实现 Pascal 的解释器,但是你也可以使用其它任何语言来实现它,思想的表达不应局限于任何特定的语言。



好了,让我们开始行动吧。预备,开始!

你将通过编写一个四则运算表达式的解释器(俗称计算器),来完成对解释器和编译器的第一次进军。今天的目标很简单:让计算器能够处理个位整数的加法,比如 3 + 5。下面是你的计算器的代码,对不起,是你的解释器的代码。

# Token types
#
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, EOF = 'INTEGER', 'PLUS', 'EOF'

class Token(object):
    def __init__(self, type, value):
        # token type: INTEGER, PLUS, or EOF
        self.type = type
        # token value: 0, 1, 2. 3, 4, 5, 6, 7, 8, 9, '+', or None
        self.value = value

    def __str__(self):
        """String representation of the class instance.

        Examples:
            Token(INTEGER, 3)
            Token(PLUS '+')
        """
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )

    def __repr__(self):
        return self.__str__()

class Interpreter(object):
    def __init__(self, text):
        # client string input, e.g. "3+5"
        self.text = text
        # self.pos is an index into self.text
        self.pos = 0
        # current token instance
        self.current_token = None

    def error(self):
        raise Exception('Error parsing input')

    def get_next_token(self):
        """Lexical analyzer (also known as scanner or tokenizer)

        This method is responsible for breaking a sentence
        apart into tokens. One token at a time.
        """
        text = self.text

        # is self.pos index past the end of the self.text ?
        # if so, then return EOF token because there is no more
        # input left to convert into tokens
        if self.pos > len(text) - 1:
            return Token(EOF, None)

        # get a character at the position self.pos and decide
        # what token to create based on the single character
        current_char = text[self.pos]

        # if the character is a digit then convert it to
        # integer, create an INTEGER token, increment self.pos
        # index to point to the next character after the digit,
        # and return the INTEGER token
        if current_char.isdigit():
            token = Token(INTEGER, int(current_char))
            self.pos += 1
            return token

        if current_char == '+':
            token = Token(PLUS, current_char)
            self.pos += 1
            return token

        self.error()

    def eat(self, token_type):
        # compare the current token type with the passed token
        # type and if they match then "eat" the current token
        # and assign the next token to the self.current_token,
        # otherwise raise an exception.
        if self.current_token.type == token_type:
            self.current_token = self.get_next_token()
        else:
            self.error()

    def expr(self):
        """expr -> INTEGER PLUS INTEGER"""
        # set current token to the first token taken from the input
        self.current_token = self.get_next_token()

        # we expect the current token to be a single-digit integer
        left = self.current_token
        self.eat(INTEGER)

        # we expect the current token to be a '+' token
        op = self.current_token
        self.eat(PLUS)

        # we expect the current token to be a single-digit integer
        right = self.current_token
        self.eat(INTEGER)
        # after the above call the self.current_token is set to
        # EOF token

        # at this point INTEGER PLUS INTEGER sequence of tokens
        # has been successfully found and the method can just
        # return the result of adding two integers, thus
        # effectively interpreting client input
        result = left.value + right.value
        return result

def main():
    while True:
        try:
            # To run under Python3 replace 'raw_input' call
            # with 'input'
            text = raw_input('calc> ')
        except EOFError:
            break
        if not text:
            continue
        interpreter = Interpreter(text)
        result = interpreter.expr()
        print(result)

if __name__ == '__main__':
    main()
将上面的代码保存为文件 calc1.py ,或者直接从 Github 上下载。在深入代码之前,以命令行运行,观察其运行结果。好好把玩这个程序。这是在我的笔记本上的一个示例会话(如果你使用 Python3,代码中 raw_input 需改写为 input )

$ python calc1.py
calc> 3+4
7
calc> 3+5
8
calc> 3+9
12
calc>
为了让计算器工作正常,不抛出异常,输入应该遵循几条规则:

输入只能是个位数的整数
当前支持的算术运算只有加法
在输入中不允许出现空格
为了使计算器简单这些限制是必要的。别担心,很快你将使它变得很复杂。

好了,现在让我们深入理解解释器是怎样工作的,以及它是怎样计算算术表达式的。

当你在命令行下输入 3+5 时,解释器得到一串字符 “3+5”。为了让解释器真正地理解怎么处理这一字符串,第一步需要将 “3+5”切分成不同的部分,我们称之为记号(tokens)。一个记号(token)是一对类型·值。举例来说,记号 “3”的类型为 INTEGER,相对应的值为整数3。

将输入的字符串切分成记号的过程被称作词法分析。所以,第一步解释器需要读取输入并把它转换成一系列的记号。解释器做这部分工作的组件被称作词法分析器(lexical ananlyzer,简称lexer)。你也许碰到过其它的叫法,像扫描程序(scanner),分词器(tokenizer)。它们意思都相同:解释器或者编译器中把输入字符串转换成一串记号的组件。

Interpreter 类中的 get_next_token 方法就是一个词法分析器。每当你调用它,就能得到从传入的字符串中创建的记号里的下一个。我们来仔细看看这个方法,看它是怎样把字符串转换成记号的。输入的字符串被保存在变量 text 中,pos 是字符串的一个索引值(把字符串想象成一个字符的数组)。pos 被初始化成 0,指向字符 ‘3’。get_next_token 首先测试这个字符是不是一个数字。如果是,pos 加 1 右移并返回一个类型是 INTEGER 值为 3 的 Token 实例,代表一个整型数字 3:

现在 pos 指向 text 中的字符 ‘+’。当你下次调用 get_next_token 方法时,它先测试在 pos 位置的字符是不是一个数字,然后测试它是不是一个加号,现在它的确是加号。于是 get_next_token 对 pos 加 1 并返回一个类型为 PLUS 值为 ‘+’ 的 Token 实例:

现在 pos 指向字符 ‘5’。当你再次调用 get_next_token 方法时,这个方法检查它是不是一个数字,它是,所以它对 pos 加 1 并且返回一个新的类型为 INTEGER,值被设置成 5 的 Token 实例。

由于现在 pos 指向字符串 “3+5”尾部的后一个位置,每次调用 get_next_token 将会返回一个 EOF Token 对象。

动手试试,看看你的计算器中词法分析器是怎样工作的:

>>> from calc1 import Interpreter
>>>
>>> interpreter = Interpreter('3+5')
>>> interpreter.get_next_token()
Token(INTEGER, 3)
>>>
>>> interpreter.get_next_token()
Token(PLUS, '+')
>>>
>>> interpreter.get_next_token()
Token(INTEGER, 5)
>>>
>>> interpreter.get_next_token()
Token(EOF, None)
>>>
现在解释器能访问从输入字符串中等到的记号流,解释器需要对这些记号做一些事:在平滑的记号流中找到一种结构,记号流是从 lexer 的 get_next_token 而来。你的解释器期望在这串记号中找到这么一种结构:INTERGER -> PLUS -> INTEGER。也就是说,解释器尝试找到这种序列的记号:整数后面跟着一个加号,再后面跟着一个整数。

查找这种结构并对其进行解释的方法是 expr。这个方法验证记号的序列是不是跟期望的一样,比如 INTEGER -> PLUS -> INTEGER。验证成功后,用 PLUS 左边的记号的值加上 PLUS 右边的记号的值就得到了结果,这样就成功地解释了传递给解释器的算术表达式。

expr 方法使用 辅助方法(helper method) eat 来验证传递给 eat 方法的记号类型与 current token 类型是否匹配。匹配成功 eat 方法获取下一个记号,并把下一个记号赋值给变量 current_token,如此实际上就把匹配的记号“吃掉了”,并把一个虚拟的指向记号流的指针向前移动了。如果在记号中的结构与所期望的 INTEGER PLUS INTEGER 序列不对等,eat 方法抛出一个异常。

概括一下解释器是怎样计算算术表达式的:

解释器接受一个输入字符串,比如说“3+5”
解释器调用 expr 方法从词法分析器 get_next_token 得到的记号流中查找一种结构。这种结构的形式为:INTEGER PLUS INTEGER。在确认了这种结构后,它将通过对两个 INTEGER 的记号做加法来解释输入。对于解释器来说在,这时要做的就是把两个整数加起来,即 3 和 5。
恭喜自己吧。你刚学会怎样构建你的第一个解释器!



现在是练习时间。



你不会认为读了这篇文章就足够了,对吗?那好,不要嫌弄脏手,完成下面的练习:

修改代码,使得多位的整型数字也能做为输入,比如 “12+3”
添加一个方法处理空格,使得你的计算器能处理包含空格的输入,比如 “  12 + 3”
修改代码用 ‘-’ 代替 ‘+’,使其能计算像 “7-5” 的表达式


检查你是否理解了

1.什么是解释器?
2.什么是编译器?
3.解释器和编译器何不同?
4.什么是记号(token)?
5.将输入切分成记号的过程名叫什么?
6.解释器中负责词法分析的部分叫什么?
7.这部分在解释器及编译器中共同的名字叫什么?

在结束这篇文章前,我希望你能保证学习解释器和编译器。并且我希望你马上做这件事。不要把它放在一边。不要等。如果你已经略读了这篇文章,请再看一遍。如果你已经仔细阅读但没做练习——现在就做。如果你只做了部分练习,完成剩下的。签下承诺保证,今天就开始学习解释器和编译器!

我,      ,身体健康,思想健全,在此郑重保证从今天开始学习解释器和编译器直到有一天 100% 知道它们是怎么工作的!

签名:
日期:


签上你的名字、日期,把它放在你能天天看到的地方,确保坚持你的承诺。并且记住承诺的定义:

“承诺就是做你曾经说过要做的事,即使说这话时的好心情早已不在了。”——Darren Hardy

好了,今天就这么多了,在这个小系列的下一篇文章中,你将得以扩展你的计算器,让它支持更多的算术运算。敬请期待。

如果你不想等本系列的第二部分,并且迫不及待地想开始深入解释器和编译器,这有我推荐的一张有帮助的书单。

1.《编程语言实现模式》(实用主义程序员)
2.《Writing Compilers and Interpreters: A Software Engineering Approach》
3.《现代编译原理——java语言描述》
4.《现代编译程序设计》
5.《编译原理技术和工具》(第二版)
一起来写个简单的解释器(2):两个整数相加或相减
Burger 和 Starbird 在其著作《高效思考的 5 要素》中分享了他们如何观察 Tony Plog 的故事。Tony Plog是一名享誉世界的小号艺术家,经营了一家针对熟练小号演奏者的高级讲习班。学生们一开始就可以熟练地演奏复杂的乐句,他们演奏得很棒。然后他们被要求演奏非常基础的、简单的音符。当他们演奏这些音符时,这些音符和先前被演奏的复杂乐句相比,听起来就很稚嫩了。他们演奏完毕后,主讲老师也演奏了同样的音符,不过他演奏时,这些音符听起来并不稚嫩。区别是令人震撼的。Tony 解释说掌握简单音符的演奏使得演奏者可以更完美地掌握复杂的片段。结论很清楚了:要想成为真正的艺术家,你必须集中精力掌握简单、基础的概念。

故事的意义很显然不仅适用于音乐,也适合软件开发。这个故事对我们所有人都是一个很好的提示:不要忽视去深入理解简单、基础的概念,即使有时候这看起来像是在走回头路。成为一个你所使用的工具或框架的专家固然重要,但了解背后的原理也是极为重要的。正如拉尔夫·沃尔多·爱默生所说:

“如果你仅仅学习方法,那么你将被方法所束缚。但是如果你学习原理,你将能够设计自己的方法。”
基于这样的观点,让我们再次深入解释器和编译器吧。

今天展示在第一篇中计算器的基础上出一个新版本,新版计算器能够:

1. 处理输入字符串中的空白字符;
2. 处理输入中的多位整数;
3. 两整数相减(现在只能相加);
这里是完成上述所有功能的新版计算器的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# Token types
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, MINUS, EOF = 'INTEGER', 'PLUS', 'MINUS', 'EOF'

class Token(object):
    def __init__(self, type, value):
        
# token type: INTEGER, PLUS, MINUS, or EOF
        self.type = type
        
# token value: non-negative integer value, '+', '-', or None
        self.value = value

    def __str__(self):
        
"""String representation of the class instance.

        
Examples:
            
Token(INTEGER, 3)
            
Token(PLUS '+')
        
"""
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )

    def __repr__(self):
        return self.__str__()

class Interpreter(object):
    def __init__(self, text):
        
# client string input, e.g. "3 + 5", "12 - 5", etc
        self.text = text
        
# self.pos is an index into self.text
        self.pos = 0
        
# current token instance
        self.current_token = None
        self.current_char = self.text[self.pos]

    def error(self):
        raise Exception('Error parsing input')

    def advance(self):
        
"""Advance the 'pos' pointer and set the 'current_char' variable."""
        self.pos += 1
        if self.pos > len(self.text) - 1:
            self.current_char = None  
# Indicates end of input
        else:
            self.current_char = self.text[self.pos]

    def skip_whitespace(self):
        while self.current_char is not None and self.current_char.isspace():
            self.advance()

    def integer(self):
        
"""Return a (multidigit) integer consumed from the input."""
        result = ''
        while self.current_char is not None and self.current_char.isdigit():
            result += self.current_char
            self.advance()
        return int(result)

    def get_next_token(self):
        
"""Lexical analyzer (also known as scanner or tokenizer)

        
This method is responsible for breaking a sentence
        
apart into tokens.
        
"""
        while self.current_char is not None:

            if self.current_char.isspace():
                self.skip_whitespace()
                continue

            if self.current_char.isdigit():
                return Token(INTEGER, self.integer())

            if self.current_char == '+':
                self.advance()
                return Token(PLUS, '+')

            if self.current_char == '-':
                self.advance()
                return Token(MINUS, '-')

            self.error()

        return Token(EOF, None)

    def eat(self, token_type):
        
# compare the current token type with the passed token
        
# type and if they match then "eat" the current token
        
# and assign the next token to the self.current_token,
        
# otherwise raise an exception.
        if self.current_token.type == token_type:
            self.current_token = self.get_next_token()
        else:
            self.error()

    def expr(self):
        
"""Parser / Interpreter

        
expr -> INTEGER PLUS INTEGER
        
expr -> INTEGER MINUS INTEGER
        
"""
        
# set current token to the first token taken from the input
        self.current_token = self.get_next_token()

        
# we expect the current token to be an integer
        left = self.current_token
        self.eat(INTEGER)

        
# we expect the current token to be either a '+' or '-'
        op = self.current_token
        if op.type == PLUS:
            self.eat(PLUS)
        else:
            self.eat(MINUS)

        
# we expect the current token to be an integer
        right = self.current_token
        self.eat(INTEGER)
        
# after the above call the self.current_token is set to
        
# EOF token

        
# at this point either the INTEGER PLUS INTEGER or
        
# the INTEGER MINUS INTEGER sequence of tokens
        
# has been successfully found and the method can just
        
# return the result of adding or subtracting two integers,
        
# thus effectively interpreting client input
        if op.type == PLUS:
            result = left.value + right.value
        else:
            result = left.value - right.value
        return result

def main():
    while True:
        try:
            
# To run under Python3 replace 'raw_input' call
            
# with 'input'
            text = raw_input('calc> ')
        except EOFError:
            break
        if not text:
            continue
        interpreter = Interpreter(text)
        result = interpreter.expr()
        print(result)

if __name__ == '__main__':
    main()
将上述源码保存为 calc2.py 文件,或直接从 GitHub 下载。试一试,看看它是不是如想象中一样工作:它能够接受多位整数,并且它能够象两数相加一样将两数相减。

Here is a sample session that I ran on my laptop:

这里是我笔记本上运行的一个例子:

1
2
3
4
5
6
$ python calc2.py
calc> 27 + 3
30
calc> 27 - 7
20
calc>
与 第一篇中的版本相比,最主要的变化有:

get_next_token 方法稍微重构了一点。pos 指针的自增逻辑放到了一个独立的 advance 方法。
增加了两个方法:skip_whitespace 方法用来忽略空白字符,integer 方法用来处理输入中的多位整数。
修改了 expr 方法,在识别 INTEGER -> PLUS -> INTEGER 短语的基础上,识别 INTEGER -> MINUS -> INTEGER 短语。在成功识别相应短语后,这个方法现在也能够解释加法和减法了。
在第一部分,我们学习了两个重要的概念,那就是 token(记号) 和 lexical analyzer(词法分析器)。第二篇我们来浅谈一下 lexeme、parsing 和 parser。

你已经知道 token 了。但是为了完成对 token 的讨论我需要提到 lexeme。什么是 lexeme? lexeme 就是组成 token 的字符序列。在下面的图片中你可以看到一些 token 和 lexeme 的示例,希望已经把它们之间的关系表达清楚了:



现在,还记得我们的朋友 expr 方法吗?我曾经说过,这里是对一个算术表达式的解释真正发生的地方。但是在你对一个表达式进行解释之前,你首先需要识别短语的类型,例如:是加法还是减法。这是 expr 方法本质上干的活:它从 get_next_token 方法获取 token 流,找到 token 流的结构,解释识别出的短语,生成算术表达式的结果。

找到 token 流中的结构的过程,换句话说,识别 token 流中的短语的过程称之为 parsing(语法分析)。解释器或编译器中用以完成这样的工作的部分,称之为 parser(语法分析器)。

现在你知道了 expr 方法是解释器中既做了 parsing(语法分析)又做了 interpreting(解释)的部分。expr 方法首先试图识别(parsing)token 流中的 INTEGER -> PLUS -> INTEGER 或 INTEGER -> MINUS -> INTEGER 短语,成功完成识别(parsed)其中一种短语后,该方法解释它,并将两整数相加或相减的结果返回给调用者。

又到了练习时间。



扩展计算器,来处理两整数相乘
扩展计算器,来出来两整数相除
修改代码,以解释包含任意多加法和减法的表达式,例如:“9 – 5 + 3 + 11”
检查你是否已理解:

什么是 lexeme ?
找出 token 流中的结构的过程叫什么?换句话说,从 token 流中识别出特定短语的过程叫什么?
解释器(编译器)中做 parsing(语法分析)的部分的名称是什么?
我希望你喜欢今天的内容。在本系列的下篇文章中,你将扩展你的解释器来处理更加复杂的算术表达式。保持关注哦!



这有我推荐的一张有帮助的书单。

1.《编程语言实现模式》(实用主义程序员)
2.《Writing Compilers and Interpreters: A Software Engineering Approach》
3.《现代编译原理——java语言描述》
4.《现代编译程序设计》
5.《编译原理技术和工具》(第二版)
一起来写个简单的解释器(3):任意数量的加法和减法
今早起来我就想:“为什么我们学习一项新技能这么困难?”

我不认为这仅仅是因为艰苦的工作。我认为其中一个原因是我们花费了很多时间和努力去阅读和观看,从而获取知识。然而并没有足够的时间通过实践将知识转换成技能。以游泳为例。你可以花很多时间去阅读数以百计关于游泳的书,与有经验的游泳运动员和教练聊上几个小时,看培训视频,然而在你第一次跳进游泳池里面时,你仍然会像石头一样往下沉。



最根本的是:不管你觉得你对某个科目有多了解——你必须把知识付诸实践,从而将知识转化成技能。为了帮助你去实践,我将一些练习放到了《一起来写个简单的解释器》系列文章的第一篇和第二篇中。当然,你将会在今天的这篇文章或者以后的文章中看到更多的练习,我发誓 :)

好了,让我们开始今天的文章吧,好吗?

目前,你已经学习了如何解释像“7 + 3”或者“12 – 9”这样的两个整数相加或者相减的算术表达式。今天,我将会介绍关于如何解析(识别)和解释带有任意数量加法或者减法运算符的算术表达式,例如“7 – 3 + 2 – 1”。

可以用下面的语法图来表示这篇文章的算术表达式:



语法图是什么?语法图是指表示一种程序设计语言语法规则的示意图。本质上,一个语法图直观地显示了在你的程序设计语言中,允许使用哪些语句和不允许使用哪些语句。

语法图十分易于阅读:只需跟随箭头指示的路径。一些路径表示选择。另一些路径表示循环。

你可以像下面一样阅读上面的语法图:一个 term 有选择地接着一个加号或者减号,再跟着另一个 term,依次是有选择地接着一个加号或者减号,再跟着另一个 term 等等。你可能会好奇一个“term”是什么。对于本文,一个“term”就是一个整数。

语法图的两个主要目的:

它们以图表的形式表示一种程序设计语言的规范(语法)。
它们可以帮助你编写解析器,你可以通过遵循简单的规则将一个图表映射成代码。
你已经知道在一连串 token 中识别出一个短语的过程称作 parsing。一个解释器或者编译器用于处理工作的部分称作 parser。parsing 也叫作语法分析(syntax analysis),而 parser 贴切地称为,你猜对了,一个语法分析器(syntax analyzer)。

根据上面的语法图,下面所有的算术表达式都是有效的:

3
3 + 4
7 – 3 + 2 – 1
因为在不同的程序设计语言中,算术表达式的语法规则十分相似,所以我们可以使用 Python shell 来“测试”我们的语法图。启动 Python shell 并且查看结果:

1
2
3
4
5
6
>>> 3
3
>>> 3 + 4
7
>>> 7 - 3 + 2 - 1
5
这里没什么特别之处。

然而表达式“3 + ”并不是一个有效的算术表达式,因为根据语法图,加号后面必须跟着一个 term(整数),否则将会是一个语法错误。再次,用 Python shell 尝试一下并且查看结果:

1
2
3
4
5
>>> 3 +
  File "<stdin>", line 1
    3 +
      ^
SyntaxError: invalid syntax
能够用 Python shell 来做一些测试当然很好,但是还是让我们将上述语法图映射成代码并使用我们自己的解释器来测试,如何?

从以前的文章(第一篇和第二篇)中,你已经知道我们的 parser 和 interpreter 位于 expr 方法中。再次说明,parser 仅仅识别结构并确保这个结构符合一些规范,一旦 parser 成功识别(解析)出结构,interpreter 会对表达式求值。

下面的代码片段介绍了相应于语法图的 parser 代码。语法图中的矩形(term)变成解析一个整数的 term 方法,而 expr 方法则遵循语法图流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def term(self):
    self.eat(INTEGER)

def expr(self):
   
# set current token to the first token taken from the input
    self.current_token = self.get_next_token()

    self.term()
    while self.current_token.type in (PLUS, MINUS):
        token = self.current_token
        if token.type == PLUS:
            self.eat(PLUS)
            self.term()
        elif token.type == MINUS:
            self.eat(MINUS)
            self.term()
可以看到 expr 首先调用了 term 方法。然后,expr 方法有一个可以执行 0 次或者多次的 while 循环。在循环中,parser 根据 token 进行判断(是一个加号还是减号)。花一些时间自行验证上面的代码的确遵循算术表达式的语法图流程。

然而 parser 本身并不会解释所有东西:如果它识别到表达式,它会沉默。如果它识别不到,它会抛出一个语法错误。下面让我们修改 expr 方法并添加 interpreter 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def term(self):
   
"""Return an INTEGER token value"""
    token = self.current_token
    self.eat(INTEGER)
    return token.value

def expr(self):
   
"""Parser / Interpreter """
   
# set current token to the first token taken from the input
    self.current_token = self.get_next_token()

    result = self.term()
    while self.current_token.type in (PLUS, MINUS):
        token = self.current_token
        if token.type == PLUS:
            self.eat(PLUS)
            result = result + self.term()
        elif token.type == MINUS:
            self.eat(MINUS)
            result = result - self.term()

    return result
因为 interpreter 需要求表达式的值,因此修改 term 方法使之返回一个整数值,修改 expr 方法使之在适当的地方执行加法和减法并且返回解释的结果。尽管代码非常简单,我还是建议你花一点时间去学习一下。

现在让我们继续往下,看看 interpreter 完整的代码,可以不?

下面是新版本计算器的源代码,该计算器可以处理包含整数和任意数量加法和减法运算符的有效的算术表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# Token types
#
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, MINUS, EOF = 'INTEGER', 'PLUS', 'MINUS', 'EOF'

class Token(object):
    def __init__(self, type, value):
        
# token type: INTEGER, PLUS, MINUS, or EOF
        self.type = type
        
# token value: non-negative integer value, '+', '-', or None
        self.value = value

    def __str__(self):
        
"""String representation of the class instance.

        
Examples:
            
Token(INTEGER, 3)
            
Token(PLUS, '+')
        
"""
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )

    def __repr__(self):
        return self.__str__()

class Interpreter(object):
    def __init__(self, text):
        
# client string input, e.g. "3 + 5", "12 - 5 + 3", etc
        self.text = text
        
# self.pos is an index into self.text
        self.pos = 0
        
# current token instance
        self.current_token = None
        self.current_char = self.text[self.pos]

   
##########################################################
   
# Lexer code                                             #
   
##########################################################
    def error(self):
        raise Exception('Invalid syntax')

    def advance(self):
        
"""Advance the `pos` pointer and set the `current_char` variable."""
        self.pos += 1
        if self.pos > len(self.text) - 1:
            self.current_char = None  
# Indicates end of input
        else:
            self.current_char = self.text[self.pos]

    def skip_whitespace(self):
        while self.current_char is not None and self.current_char.isspace():
            self.advance()

    def integer(self):
        
"""Return a (multidigit) integer consumed from the input."""
        result = ''
        while self.current_char is not None and self.current_char.isdigit():
            result += self.current_char
            self.advance()
        return int(result)

    def get_next_token(self):
        
"""Lexical analyzer (also known as scanner or tokenizer)

        
This method is responsible for breaking a sentence
        
apart into tokens. One token at a time.
        
"""
        while self.current_char is not None:

            if self.current_char.isspace():
                self.skip_whitespace()
                continue

            if self.current_char.isdigit():
                return Token(INTEGER, self.integer())

            if self.current_char == '+':
                self.advance()
                return Token(PLUS, '+')

            if self.current_char == '-':
                self.advance()
                return Token(MINUS, '-')

            self.error()

        return Token(EOF, None)

   
##########################################################
   
# Parser / Interpreter code                              #
   
##########################################################
    def eat(self, token_type):
        
# compare the current token type with the passed token
        
# type and if they match then "eat" the current token
        
# and assign the next token to the self.current_token,
        
# otherwise raise an exception.
        if self.current_token.type == token_type:
            self.current_token = self.get_next_token()
        else:
            self.error()

    def term(self):
        
"""Return an INTEGER token value."""
        token = self.current_token
        self.eat(INTEGER)
        return token.value

    def expr(self):
        
"""Arithmetic expression parser / interpreter."""
        
# set current token to the first token taken from the input
        self.current_token = self.get_next_token()

        result = self.term()
        while self.current_token.type in (PLUS, MINUS):
            token = self.current_token
            if token.type == PLUS:
                self.eat(PLUS)
                result = result + self.term()
            elif token.type == MINUS:
                self.eat(MINUS)
                result = result - self.term()

        return result

def main():
    while True:
        try:
            
# To run under Python3 replace 'raw_input' call
            
# with 'input'
            text = raw_input('calc> ')
        except EOFError:
            break
        if not text:
            continue
        interpreter = Interpreter(text)
        result = interpreter.expr()
        print(result)

if __name__ == '__main__':
    main()
将上面的代码保存到 calc3.py 文件中,或者直接在 GitHub 上下载。尝试一下。亲自看看,它可以处理从前面展示的语法图中获得的算术表达式。

下面是我在我的笔记本上运行的示例会话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ python calc3.py
calc> 3
3
calc> 7 - 4
3
calc> 10 + 5
15
calc> 7 - 3 + 2 - 1
5
calc> 10 + 1 + 2 - 3 + 4 + 6 - 15
5
calc> 3 +
Traceback (most recent call last):
  File "calc3.py", line 147, in <module>
    main()
  File "calc3.py", line 142, in main
    result = interpreter.expr()
  File "calc3.py", line 123, in expr
    result = result + self.term()
  File "calc3.py", line 110, in term
    self.eat(INTEGER)
  File "calc3.py", line 105, in eat
    self.error()
  File "calc3.py", line 45, in error
    raise Exception('Invalid syntax')
Exception: Invalid syntax
记得我在文章开头提到的那些练习么:下面就是啦,言出必行哦 :)



画一个只包含乘法和除法的算术表达式语法图,例如“7 * 4 / 2 * 3”。请认真对待,拿起一支笔或者铅笔尝试画一个。
修改计算器的源代码,使之可以解释只包含乘法和除法的算术表达式,例如“7 * 4 / 2 * 3”。
从头开始写一个 interpreter 来处理像“7 – 3 + 2 – 1”这样的算术表达式。使用任何你运用自如的程序设计语言,在不看例子的情况下写出来。当你这样做时,想一下有关的组件:lexer 带着一个输入并将其转换成一连串 token,parser 获取 lexer 提供的一连串 token 并且尝试在流中识别出一个结构,在 parser 成功地解析(识别)出一个有效的算术表达式之后,interpreter 会产生结果。把这些片段串在一起。花一些时间将你学到的知识转化成一个可以工作的算术表达式解释器。
检查你的理解。

语法图是什么?
语法分析是什么?
语法分析器是什么?
Hey,看!你读到文章的结尾了。谢谢今天呆在这里,不过不要忘记做练习哦。:) 下次我会带来一篇新的文章——敬请期待。

下面是我推荐的一些书籍,它们可以帮助你学习解释器和编译器:

《Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages (Pragmatic Programmers)》
《Writing Compilers and Interpreters: A Software Engineering Approach》
《Modern Compiler Implementation in Java》
《Modern Compiler Design》
《Compilers: Principles, Techniques, and Tools (2nd Edition)》
一起来写个简单的解释器(4):任意数量的乘法和除法
你是在被动地学习这几篇文章中的材料,还是主动地去做练习?我希望你主动地去练习。我真的希望哦 :)

记得孔子说过的话么?

“听而易忘”


“见而易记”


“做而易懂”


在上一篇文章中,你已经学会了如何解析(识别)和解释带有任意数量加法或者减法运算符的算术表达式,例如“7 – 3 + 2 – 1”。你也学习了语法图以及如何使用语法图来指定一种程序设计语言的语法。

本文你将会学习如何解析(识别)和解释带有任意数量乘法或者除法运算符的算术表达式,例如“7 * 4 / 2 * 3”。这篇文章讨论的除法是整数除法,所以如果表达式是“9 / 4”,那么答案将会是一个整数:2。

今天我也会讨论不少有关另一种被广泛使用的、用于指定一种程序设计语言语法的表示法。它叫做上下文无关文法(简称文法)或者 BNF(Backus-Naur Form 巴科斯-诺尔范式)。在这篇文章中,我不会使用纯净的 BNF 表示法,而是使用类似的修改过的 EBNF 表示法。

下面是使用文法的几个原因:

文法以简明的方式说明一种程序设计语言的语法。不像语法图,文法十分简洁。你将会看到我在未来的文章中越来越多地使用文法。
文法可以用作很好的文档。
即使你从零开始编写你的解析器,文法也是一个很好的起点。通常,你可以通过遵循一系列简单的规则将文法转换成代码。
有一组工具叫做解析器生成器,它接收一段文法作为输入,并且根据那段文法自动地生成一个解析器。我会在系列后面的文章中讨论那些工具。
现在,让我们谈一下文法的原理,好吗?

下面是描述像“7 * 4 / 2 * 3”(它只是众多可以由文法生成的表达式之一)这样的算术表达式的一段文法:



一段文法由一系列规则(rule)组成,也称为 产生式(productions)。在我们的文法中有两条规则:



一条规则由一个非终结符(称为产生式的头或者左边)、一个冒号和一系列终结符(和 | 或者)非终结符(称为产生式的主体或者右边)组成:



上面介绍的文法中,像 MUL、DIV 和 INTEGER 这样的标记称为终结符(terminals),像 expr 和 factor 这样的变量称为非终结符(non-terminals)。非终结符通常由一系列终结符(和 | 或者)非终结符组成:



第一条规则左边的非终结符符号称为开始符号(start symbol)。在我们的文法例子中,开始符号是 expr:



你可以这样读 expr 规则:“expr 可以是一个 factor 可选地接着一个乘法或者除法运算符,再接着另一个 factor,依次可选地接着一个乘法或者除法运算符,再接着另一个 factor……”

factor 是什么?就本文而言,factor 就是一个整数。

让我们快速地回顾一下文法中用到的符号和它们的含义。

| – 多选一。表示“或”。所以 (MUL | DIV) 表示要么是 MUL,要么是 DIV。
( … ) – 一对圆括号表示把终结符(和 | 或者)非终结符组合起来,就像 (MUL | DIV) 一样。
( … )* – 匹配组合里面的内容 0 次或者多次。
如果你以前了解过正则表达式,那么 |、() 和 (…)* 这些符号对于你来说应该会比较熟悉。

一段文法通过说明它可以组成什么句子来定义一种语言(language)。这是如何使用文法推导出算术表达式:首先以开始符号 expr 开始,然后反复地用一个非终结符所在规则的主体替代该非终结符,直到产生一个只包含终结符的句子。那些句子组成了由文法定义的语言。

如果文法不能得到一条确定的算术表达式,那么它不支持该表达式,并且当解析器尝试识别该表达式时,解析器会生成一个语法错误。

我依次想了几个例子。下面是文法如何得到表达式 3 的例子:



这是文法如何得到表达式 3 * 7 的例子:



这是文法如何得到表达式 3 * 7 / 2 的例子:



哇,这里有相当多的理论!

我想当我第一次阅读关于文法相关的术语和诸如此类的东西,我有这样一种感觉:



我可以向你保证我肯定不会像这样:



我花费了一些时间来适应这种表示法,它是如何工作的和它与解析器、语法分析器的关系。但是我必须告诉你,从长远来看,学习它是值得的。因为它在实际中被广泛应用,你也一定会遇到一些编译器文献在某些时候会用到它。所以,为何不早点学呢?:)

现在,让我们将文法映射成代码,可以么?

这里是用于将文法转换成源代码的准则。遵循这些准则,你可以逐字逐句地把文法翻译给正在工作的分析器:

文法中定义的每条规则,R,会变成一个同名的方法,而对那条规则的引用变成一个方法调用:R()。方法的主体跟着同一套准则的规则的主体流。
多个可选项 (a1 | a2 | aN) 变成 if-elif-else 语句。
可选的 (…)* 集合变成 while 语句,该语句可以循环 0 次或者多次。
每个符号引用 T 变成对 eat 方法的调用:eat(T)。eat 方法的工作方式是如果该方法匹配当前的 lookahead 符号,那么 eat 方法会传入符号 T,然后它会从词法分析器中得到一个新的符号,并且把该符号分配给内部变量 current_token。
这些准则直观上看起来像这样:



让我们继续前进,遵循上述准则将我们的文法转换成代码。

在我们的文法中有两条规则:expr 规则和 factor 规则。我们以 factor 规则(产生式)开始。根据准则,你需要创建一个名为 factor 的方法(准则 1),该方法有一个对以 INTEGER 符号为参数的 eat 方法的调用(准则 4):

1
2
def factor(self):
    self.eat(INTEGER)
这很简单,不是吗?

继续!

expr 规则变成 expr 方法(再次依据准则 1)。该规则开始是对变成了 factor() 方法调用的 factor 的引用。可选的集合 (…)* 变成一个 while 循环,(MUL | DIV) 变成 if-elif-else 语句。把那些代码段组合在一起,我们得到了下面的 expr 方法:

1
2
3
4
5
6
7
8
9
10
11
def expr(self):
    self.factor()

    while self.current_token.type in (MUL, DIV):
        token = self.current_token
        if token.type == MUL:
            self.eat(MUL)
            self.factor()
        elif token.type == DIV:
            self.eat(DIV)
            self.factor()
请花点时间学习一下我是如何将文法映射成源代码的。确保你弄懂了那部分的内容,因为在以后会派上用场的。

为了方便,我将上述的代码放到 parser.py 文件中,它包含一个词法分析器和一个解析器,不包含解释器。你可以直接从 GitHub 下载文件并使用它。该程序有交互式提示,你可以在提示中输入表达式并查看表达式是否可用:也就是说,依据文法构建的解析器是否可以识别该表达式。

下面是运行在我的电脑上一段简单的会话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ python parser.py
calc> 3
calc> 3 * 7
calc> 3 * 7 / 2
calc> 3 *
Traceback (most recent call last):
  File "parser.py", line 155, in <module>
    main()
  File "parser.py", line 151, in main
    parser.parse()
  File "parser.py", line 136, in parse
    self.expr()
  File "parser.py", line 130, in expr
    self.factor()
  File "parser.py", line 114, in factor
    self.eat(INTEGER)
  File "parser.py", line 107, in eat
    self.error()
  File "parser.py", line 97, in error
    raise Exception('Invalid syntax')
Exception: Invalid syntax
试一下!

我忍不住再次提到语法图。这是对于同一个 expr 规则的语法图:



我们是时候专研一下新的算术表达式解释器的源代码了。下面的代码是一个计算器,该计算器可以处理包含整数和任意数量的乘法和除法(整除)运算符的有效算术表达式。你也可以看到我把词法分析器重构成一个 Lexer 类,并且把 Lexer 实例作为参数更新 Interpreter 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# Token types
#
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, MUL, DIV, EOF = 'INTEGER', 'MUL', 'DIV', 'EOF'

class Token(object):
    def __init__(self, type, value):
        
# token type: INTEGER, MUL, DIV, or EOF
        self.type = type
        
# token value: non-negative integer value, '*', '/', or None
        self.value = value

    def __str__(self):
        
"""String representation of the class instance.

        
Examples:
            
Token(INTEGER, 3)
            
Token(MUL, '*')
        
"""
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )

    def __repr__(self):
        return self.__str__()

class Lexer(object):
    def __init__(self, text):
        
# client string input, e.g. "3 * 5", "12 / 3 * 4", etc
        self.text = text
        
# self.pos is an index into self.text
        self.pos = 0
        self.current_char = self.text[self.pos]

    def error(self):
        raise Exception('Invalid character')

    def advance(self):
        
"""Advance the `pos` pointer and set the `current_char` variable."""
        self.pos += 1
        if self.pos > len(self.text) - 1:
            self.current_char = None  
# Indicates end of input
        else:
            self.current_char = self.text[self.pos]

    def skip_whitespace(self):
        while self.current_char is not None and self.current_char.isspace():
            self.advance()

    def integer(self):
        
"""Return a (multidigit) integer consumed from the input."""
        result = ''
        while self.current_char is not None and self.current_char.isdigit():
            result += self.current_char
            self.advance()
        return int(result)

    def get_next_token(self):
        
"""Lexical analyzer (also known as scanner or tokenizer)

        
This method is responsible for breaking a sentence
        
apart into tokens. One token at a time.
        
"""
        while self.current_char is not None:

            if self.current_char.isspace():
                self.skip_whitespace()
                continue

            if self.current_char.isdigit():
                return Token(INTEGER, self.integer())

            if self.current_char == '*':
                self.advance()
                return Token(MUL, '*')

            if self.current_char == '/':
                self.advance()
                return Token(DIV, '/')

            self.error()

        return Token(EOF, None)

class Interpreter(object):
    def __init__(self, lexer):
        self.lexer = lexer
        
# set current token to the first token taken from the input
        self.current_token = self.lexer.get_next_token()

    def error(self):
        raise Exception('Invalid syntax')

    def eat(self, token_type):
        
# compare the current token type with the passed token
        
# type and if they match then "eat" the current token
        
# and assign the next token to the self.current_token,
        
# otherwise raise an exception.
        if self.current_token.type == token_type:
            self.current_token = self.lexer.get_next_token()
        else:
            self.error()

    def factor(self):
        
"""Return an INTEGER token value.

        
factor : INTEGER
        
"""
        token = self.current_token
        self.eat(INTEGER)
        return token.value

    def expr(self):
        
"""Arithmetic expression parser / interpreter.

        
expr   : factor ((MUL | DIV) factor)*
        
factor : INTEGER
        
"""
        result = self.factor()

        while self.current_token.type in (MUL, DIV):
            token = self.current_token
            if token.type == MUL:
                self.eat(MUL)
                result = result * self.factor()
            elif token.type == DIV:
                self.eat(DIV)
                result = result / self.factor()

        return result

def main():
    while True:
        try:
            
# To run under Python3 replace 'raw_input' call
            
# with 'input'
            text = raw_input('calc> ')
        except EOFError:
            break
        if not text:
            continue
        lexer = Lexer(text)
        interpreter = Interpreter(lexer)
        result = interpreter.expr()
        print(result)

if __name__ == '__main__':
    main()
将上面的代码保存到 calc4.py 文件或者直接从 GitHub 上下载。像往常一样,亲自试一下并看下它是如何工作的。

这是运行在我的笔记本上一段简单的会话:

1
2
3
4
5
6
7
$ python calc4.py
calc> 7 * 4 / 2
14
calc> 7 * 4 / 2 * 3
42
calc> 10 * 4  * 2 * 3 / 8
30
我知道你迫不及待这一部分了 :) 这是今天新的练习:



写一段文法来描述包含任意数量的 +、-、* 或者 / 运算符的算术表达式。你应该有能力从文法中得到像“2 + 7 * 4”、“7 – 8 / 4”、“14 + 2 * 3 – 6 / 2” 等这样的表达式。
使用文法,编写一个解释器,该解释器可以计算包含任意数量的 +、-、* 或者 / 运算符的算术表达式。你的解释器应该可以处理像“2 + 7 * 4”、“7 – 8 / 4”、“14 + 2 * 3 – 6 / 2” 等这样的表达式。
如果你完成了上述的联系,那么放松一下把 :)
检测你的理解。

牢记今天这篇文章介绍的文法,回答下面的几个问题,需要时可以参考下面的图片:

lsbasi_part4_bnf1

什么是上下文无关文法?
文法有多少条规则或者产生式?
什么是终结符?(在图片中找到所有的终结符)
什么是非终结符?(在图片中找到所有的非终结符)
什么是一条规则的头部?(在图片中找到所有的头部或者左边)
什么是一条规则的主体?(在图片中找到所有的主体或者右边)
文法的开始符号是什么?
嘿,你读到最后了!这篇文章包含了相当多理论,所以我真的为你读完这篇文章而感到骄傲。

下次我会带来一篇新的文章——敬请期待,不要忘记做练习哦,它们对你有好处的。

下面是我推荐的一些书籍列表,它们对你学习解释器和编译器有帮助:

Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages (Pragmatic Programmers)
Writing Compilers and Interpreters: A Software Engineering Approach
Modern Compiler Implementation in Java
Modern Compiler Design
Compilers: Principles, Techniques, and Tools (2nd Edition)
一起来写个简单的解释器(5):加减乘除表达式
如何创建一个解释器或编译器这么复杂的问题,你会如何处理呢?开始的时候它很像是一团乱糟糟的毛线,你得重新梳理展开,然后缠成一个完美的毛线球 。

达到上述目的的方法只需一次解开一根线、一个结。虽然有时候你可能会觉得你无法马上理解某些事情,但是你必须坚持下去。我保证如果你足够坚持,最后你会“豁然开朗”(哎呀呀,如果每次我不能马上弄懂某些事情的时候,我都存 25美分,那么我早就变成富豪了 :))。

关于理解如何创建一个解释器和编译器,也许我能给你的最好建议之一就说阅读本系列文章的解释、代码,然后自己去编写代码,甚至在一段时间内多次编写同样的代码,使得这些材料和代码对于你来说是很自然的。直到那时才继续学习新的主题。不要着急,请慢下来,花时间去深刻地理解基础概念。虽然这种方法看起来有点慢,但是你会受益匪浅。相信我。

你在最后终究会得到完美的毛线球。你知道吗?即使它不够完美,但是总比什么都不做和不学习这些课题,或者走马观花然后几天之后就忘记了要好。

记住——只需要坚持不懈地解开缠绕:一次一根线、一个结,并且通过编写大量代码来实践你所学过的:



今天你将会用到在本系列前面几篇文章中学到的所有知识,并且学习如何解析和解释带有任意数量的加法、减法、乘法和除法运算符的算术表达式。你将会编写一个可以计算像“14 + 2 * 3 – 6 / 2”这样的表达式的解释器。

在深入研究和编写代码之前,让我们讨论一下运算符的结合律和优先级。

按照约定,7 + 3 + 1 等同 (7 + 3) + 1,7 – 3 – 1 等同 (7 – 3) – 1。这里没有什么可惊讶的。我们在某个时候学过那些约定,并且从那以后把那些约定当作是理所当然的。如果我们把 7 – 3 – 1 当作是 7 – (3 – 1),那么结果会是 5 而不是预期的 3。

在普通的算术运算和大部分编程语言中,加法、减法、乘法和除法都是左结合:

1
2
3
4
7 + 3 + 1 is equivalent to (7 + 3) + 1
7 - 3 - 1 is equivalent to (7 - 3) - 1
8 * 4 * 2 is equivalent to (8 * 4) * 2
8 / 4 / 2 is equivalent to (8 / 4) / 2
一个运算符是左结合是什么意思?

当一个操作数像表达式 7 + 3 + 1 中的 3 一样两边都有加号时,我们需要一个约定来决定哪个运算符适用于 3。是操作数 3 左边的运算符还是右边的?因为两边都有加号的操作数属于它左边的运算符,所以 + 运算符结合左边的操作数。因此我们说运算符 + 是左结合。那就是为什么根据结合律,7 + 3 + 1 等同于 (7 + 3) + 1。

好,那么像 7 + 5 * 2 这样的表达式中的操作数 5,两边有着不同种类的运算符会是怎样呢?该表达式等同于 7 + (5 * 2) 还是 (7 + 5) * 2?我们如何解决这个歧义呢?

在这个例子中,结合律对我们已经没有帮助了。因为它只适用于同一种运算符,要么是加法类(+、-),要么是乘法类(*、/)。当在同一条表达式中有不同种类的运算符时,我们需要另一个约定来解决歧义。我们需要定义了运算符相对优先级的约定。

这就是了:如果运算符 * 比 + 先接受操作数,那么我们说 * 有更高的优先级。在我们知道和使用的算术运算中,乘法和除法比加法和减法优先级更高。所以表达式 7 + 5 * 2 等同于 7 + (5 * 2),表达式 7 – 8 / 4 等同于 7 – (8 / 4)。

在一个例子中,表达式的运算符有同样的优先级,我们只需运用结合律并且从左到右执行运算符:

1
2
7 + 3 - 1 is equivalent to (7 + 3) - 1
8 / 4 * 2 is equivalent to (8 / 4) * 2
讨论这么多关于运算符的结合律和优先级,我希望你不要认为我想让你无聊死。那些约定的好处是我们可以从一个展示算术运算符的结合律和优先级的表格中构建算术表达式的文法。然后我们可以遵循我在《一起来写个简单的解释器(4)》文章中概括的准则,将文法翻译成代码,我们的解释器也有能力处理除了结合律之外的运算符优先级。

好了,下面是我们的优先级表格:



在表格中,运算符 + 和 – 有着相同的优先级,它们都是左结合的。你也可以看到运算符 * 和 / 也是左结合,有着相同的优先级,但是它们的优先级比加法和减法运算符的优先级要高。

下面是如何根据优先级表格构建文法的规则:

为每个优先级定义一个非终结符。非终结符所在产生式的主体应该包含同等级的算术运算符和优先级高一级的非终结符。
为表达式创建一个额外的非终结符 factor 作为基本单位,在我们的例子中该基本单位是整数。通用的规则是如果你有 N 层优先级,那么你总共需要 N + 1 个非终结符:每层优先级需要一个非终结符,加上一个用作表达式基本单位的非终结符。
继续前进!

下面我们根据这些规则来构造文法。

根据规则 1,我们要定义两个非终结符:用于等级 2 的非终结符称为 expr,用于等级 1 的非终结符称为 term。而根据规则 2,我们将定义一个 factor (即一个整数)用作算术表达式的基本单位。

新文法的开始符号是 expr,expr 产生式包含一个主体,该主体使用来自等级 2 的运算符(在我们的例子中是 + 和 – 运算符)。同时 expr 产生式也包含更高优先级(等级 1)的 term 非终结符:



term 表达式包含一个主体,该主题使用来自等级 1 的运算符(在我们的例子中是 * 和 /)。同时 term 产生式也包含用作表达式基本单位的 factor(即整数):



而非终结符 factor 的产生式是:



我们已经在前几篇文章中看过上述产生式的文法和语法图形式,下面我们将会把它们结合到一个文法中,该文法会关注到运算符的结合律和优先级:



下面是与上述文法相对应的语法图:



框图中的每个矩形框是对另一个框图的“方法调用”。对于表达式 7 + 5 * 2,如果你从最上面的框图 expr 开始一直往下看到最下面的框图 factor,那么你应该可以看到较下面的框图中更高级的运算符 * 和 / 会比较上面的框图中运算符 + 和 – 先执行。

为了彻底地说明运算符的优先级,下面我们看一下根据上述的文法和语法图对同一个算术表达式 7 + 5 * 2 的分解。这只是从另一方面展示更高优先级的运算符会比低优先级运算符先执行:



好的,下面根据《一起来写个简单的解释器(4)》中的准则将文法转换成代码,再看下我们的新解释器是怎么工作的,好吗?

再次列出文法:



下面是一个计算器的全部代码,该计算器可以处理包含整数和任意数量的加法、减法、乘法和除法(整除)运算符的有效算术表达式。

下面是对比《一起来写个简单的解释器(4)》中的代码的主要修改:

Lexer 类现在可以标记 +、-、* 和 /(这里没有新的东西,我们只是把以前的代码整合到一个类中,从而支持所有的标记)
回想一下,文法中定义的每条规则(产生式),R,会变成一个同名的方法,而对那条规则的引用变成一个方法调用:R()。因此 Interpreter 类现在有三个方法,分别对应文法中的非终结符:expr、term 和 factor。
源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# Token types
#
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, MINUS, MUL, DIV, EOF = (
    'INTEGER', 'PLUS', 'MINUS', 'MUL', 'DIV', 'EOF'
)

class Token(object):
    def __init__(self, type, value):
        
# token type: INTEGER, PLUS, MINUS, MUL, DIV, or EOF
        self.type = type
        
# token value: non-negative integer value, '+', '-', '*', '/', or None
        self.value = value

    def __str__(self):
        
"""String representation of the class instance.

        
Examples:
            
Token(INTEGER, 3)
            
Token(PLUS, '+')
            
Token(MUL, '*')
        
"""
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )

    def __repr__(self):
        return self.__str__()

class Lexer(object):
    def __init__(self, text):
        
# client string input, e.g. "3 * 5", "12 / 3 * 4", etc
        self.text = text
        
# self.pos is an index into self.text
        self.pos = 0
        self.current_char = self.text[self.pos]

    def error(self):
        raise Exception('Invalid character')

    def advance(self):
        
"""Advance the `pos` pointer and set the `current_char` variable."""
        self.pos += 1
        if self.pos > len(self.text) - 1:
            self.current_char = None  
# Indicates end of input
        else:
            self.current_char = self.text[self.pos]

    def skip_whitespace(self):
        while self.current_char is not None and self.current_char.isspace():
            self.advance()

    def integer(self):
        
"""Return a (multidigit) integer consumed from the input."""
        result = ''
        while self.current_char is not None and self.current_char.isdigit():
            result += self.current_char
            self.advance()
        return int(result)

    def get_next_token(self):
        
"""Lexical analyzer (also known as scanner or tokenizer)

        
This method is responsible for breaking a sentence
        
apart into tokens. One token at a time.
        
"""
        while self.current_char is not None:

            if self.current_char.isspace():
                self.skip_whitespace()
                continue

            if self.current_char.isdigit():
                return Token(INTEGER, self.integer())

            if self.current_char == '+':
                self.advance()
                return Token(PLUS, '+')

            if self.current_char == '-':
                self.advance()
                return Token(MINUS, '-')

            if self.current_char == '*':
                self.advance()
                return Token(MUL, '*')

            if self.current_char == '/':
                self.advance()
                return Token(DIV, '/')

            self.error()

        return Token(EOF, None)

class Interpreter(object):
    def __init__(self, lexer):
        self.lexer = lexer
        
# set current token to the first token taken from the input
        self.current_token = self.lexer.get_next_token()

    def error(self):
        raise Exception('Invalid syntax')

    def eat(self, token_type):
        
# compare the current token type with the passed token
        
# type and if they match then "eat" the current token
        
# and assign the next token to the self.current_token,
        
# otherwise raise an exception.
        if self.current_token.type == token_type:
            self.current_token = self.lexer.get_next_token()
        else:
            self.error()

    def factor(self):
        
"""factor : INTEGER"""
        token = self.current_token
        self.eat(INTEGER)
        return token.value

    def term(self):
        
"""term : factor ((MUL | DIV) factor)*"""
        result = self.factor()

        while self.current_token.type in (MUL, DIV):
            token = self.current_token
            if token.type == MUL:
                self.eat(MUL)
                result = result * self.factor()
            elif token.type == DIV:
                self.eat(DIV)
                result = result / self.factor()

        return result

    def expr(self):
        
"""Arithmetic expression parser / interpreter.

        
calc>  14 + 2 * 3 - 6 / 2
        
17

        
expr   : term ((PLUS | MINUS) term)*
        
term   : factor ((MUL | DIV) factor)*
        
factor : INTEGER
        
"""
        result = self.term()

        while self.current_token.type in (PLUS, MINUS):
            token = self.current_token
            if token.type == PLUS:
                self.eat(PLUS)
                result = result + self.term()
            elif token.type == MINUS:
                self.eat(MINUS)
                result = result - self.term()

        return result

def main():
    while True:
        try:
            
# To run under Python3 replace 'raw_input' call
            
# with 'input'
            text = raw_input('calc> ')
        except EOFError:
            break
        if not text:
            continue
        lexer = Lexer(text)
        interpreter = Interpreter(lexer)
        result = interpreter.expr()
        print(result)

if __name__ == '__main__':
    main()
将上述代码保存到 calc5.py 文件中,或者直接从 GitHub 上下载。像往常一样,自己尝试一下,看下解释器是否可以正确地计算出带有不同优先级运算符的算术表达式。

下面是运行在我的电脑上一段简单的会话:

1
2
3
4
5
6
7
8
9
$ python calc5.py
calc> 3
3
calc> 2 + 7 * 4
30
calc> 7 - 8 / 4
5
calc> 14 + 2 * 3 - 6 / 2
17
下面是今天的练习:



不要窥视文章中的代码,不假思索地编写一个类似这篇文章中描述的解释器。为你的解释器写一些测试,确保都通过这些测试。
扩展解释器来处理包含括号的算术表达式,使得你的解释器可以计算像 7 + 3 * (10 / (12 / (3 + 1) – 1)) 这样深层嵌套的算术表达式。
检测你的理解。

运算符是左结合是什么意思?
运算符 + 和 – 是左结合还是右结合?运算符 * 和 / 呢?
运算符 + 的优先级比 * 高么?
嘿,你读到最后了!真是太棒了。下次我会带来一篇新的文章——敬请期待,还有不要忘记做练习哦。

下面是我推荐的一些书籍列表,它们对你学习解释器和编译器有帮助:

Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages (Pragmatic Programmers)
Writing Compilers and Interpreters: A Software Engineering Approach
Modern Compiler Implementation in Java
Modern Compiler Design
Compilers: Principles, Techniques, and Tools (2nd Edition)
一起来写个简单的解释器(6)
今天是个好日子 :) 你可能会问:“为什么?”。原因是今天我们会在文法中添加括号表达式和实现可以计算像 7 + 3 * (10 / (12 / (3 + 1) – 1)) 这样任意深度嵌套的括号表达式的解释器,然后结束关于算术表达式的讨论(好吧,差不多)。

让我们开始吧,好吗?

首先,修改文法以支持带有圆括号的表达式。记得在《一起来写个简单的解释器(5)》曾经提到,factor 规则是表达式的基本单位。在那篇文章中,我们只有一个整数作为基本单位。今天我们将会添加另一个基本单位——一个括号表达式。那么开始吧。

下面是更新后的文法:



expr 和 term 表达式与《一起来写个简单的解释器(5)》中的完全一样,唯一的变化是在 factor 产生式中,终结符 LPAREN 代表左括号 ‘(’,终结符 RPAREN 代表右括号 ‘)’,在圆括号里面的非终结符 expr 代表 expr 规则。

下面是更新过的 factor 语法图,语法图现在包括两项:



因为 expr 和 term 的文法规则没有改变,所以它们的语法图与《一起来写个简单的解释器(5)》中的是一样的:



新的文法中有一个有趣的特点——它是递归的。如果你尝试得到表达式 2 * (7 + 3),那么你会以开始符号 expr 开始,最后你还是会再次递归地使用 expr 规则,从而得到原来算术表达式的一部分 (7 + 3)。

下面根据文法来分解表达式 2 * (7 + 3) 并看下它是如何工作的:



一段题外话:如果你需要关于递归的资料,那么你可以看一下 Daniel P. Friedman 和 Matthias Felleisen 的这本书《The Little Schemer》——它真的写得很好。

好了,让我们继续将新的更新后的文法翻译成代码。

下面是对比上篇文章的代码主要修改的地方:

修改 Lexer 使之返回两个以上的标记:代表左括号的 LPAREN 和代表右括号的 PAREN。
Interpreter 的 factor 方法被稍微更新成除了可以解析整数之外,还可以解析括号表达式。
下面是一个计算器的全部代码,该计算器可以计算包含整数、任意数量的加法、减法、乘法和除法(整除)运算符和任意深度嵌套的括号表达式的算术表达式:

# Token types
#
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, MINUS, MUL, DIV, LPAREN, RPAREN, EOF = (
    'INTEGER', 'PLUS', 'MINUS', 'MUL', 'DIV', '(', ')', 'EOF'
)

class Token(object):
    def __init__(self, type, value):
        self.type = type
        self.value = value

    def __str__(self):
        """String representation of the class instance.

        Examples:
            Token(INTEGER, 3)
            Token(PLUS, '+')
            Token(MUL, '*')
        """
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )

    def __repr__(self):
        return self.__str__()

class Lexer(object):
    def __init__(self, text):
        # client string input, e.g. "4 + 2 * 3 - 6 / 2"
        self.text = text
        # self.pos is an index into self.text
        self.pos = 0
        self.current_char = self.text[self.pos]

    def error(self):
        raise Exception('Invalid character')

    def advance(self):
        """Advance the `pos` pointer and set the `current_char` variable."""
        self.pos += 1
        if self.pos > len(self.text) - 1:
            self.current_char = None  # Indicates end of input
        else:
            self.current_char = self.text[self.pos]

    def skip_whitespace(self):
        while self.current_char is not None and self.current_char.isspace():
            self.advance()

    def integer(self):
        """Return a (multidigit) integer consumed from the input."""
        result = ''
        while self.current_char is not None and self.current_char.isdigit():
            result += self.current_char
            self.advance()
        return int(result)

    def get_next_token(self):
        """Lexical analyzer (also known as scanner or tokenizer)

        This method is responsible for breaking a sentence
        apart into tokens. One token at a time.
        """
        while self.current_char is not None:

            if self.current_char.isspace():
                self.skip_whitespace()
                continue

            if self.current_char.isdigit():
                return Token(INTEGER, self.integer())

            if self.current_char == '+':
                self.advance()
                return Token(PLUS, '+')

            if self.current_char == '-':
                self.advance()
                return Token(MINUS, '-')

            if self.current_char == '*':
                self.advance()
                return Token(MUL, '*')

            if self.current_char == '/':
                self.advance()
                return Token(DIV, '/')

            if self.current_char == '(':
                self.advance()
                return Token(LPAREN, '(')

            if self.current_char == ')':
                self.advance()
                return Token(RPAREN, ')')

            self.error()

        return Token(EOF, None)

class Interpreter(object):
    def __init__(self, lexer):
        self.lexer = lexer
        # set current token to the first token taken from the input
        self.current_token = self.lexer.get_next_token()

    def error(self):
        raise Exception('Invalid syntax')

    def eat(self, token_type):
        # compare the current token type with the passed token
        # type and if they match then "eat" the current token
        # and assign the next token to the self.current_token,
        # otherwise raise an exception.
        if self.current_token.type == token_type:
            self.current_token = self.lexer.get_next_token()
        else:
            self.error()

    def factor(self):
        """factor : INTEGER | LPAREN expr RPAREN"""
        token = self.current_token
        if token.type == INTEGER:
            self.eat(INTEGER)
            return token.value
        elif token.type == LPAREN:
            self.eat(LPAREN)
            result = self.expr()
            self.eat(RPAREN)
            return result

    def term(self):
        """term : factor ((MUL | DIV) factor)*"""
        result = self.factor()

        while self.current_token.type in (MUL, DIV):
            token = self.current_token
            if token.type == MUL:
                self.eat(MUL)
                result = result * self.factor()
            elif token.type == DIV:
                self.eat(DIV)
                result = result / self.factor()

        return result

    def expr(self):
        """Arithmetic expression parser / interpreter.

        calc> 7 + 3 * (10 / (12 / (3 + 1) - 1))
        22

        expr   : term ((PLUS | MINUS) term)*
        term   : factor ((MUL | DIV) factor)*
        factor : INTEGER | LPAREN expr RPAREN
        """
        result = self.term()

        while self.current_token.type in (PLUS, MINUS):
            token = self.current_token
            if token.type == PLUS:
                self.eat(PLUS)
                result = result + self.term()
            elif token.type == MINUS:
                self.eat(MINUS)
                result = result - self.term()

        return result

def main():
    while True:
        try:
            # To run under Python3 replace 'raw_input' call
            # with 'input'
            text = raw_input('calc> ')
        except EOFError:
            break
        if not text:
            continue
        lexer = Lexer(text)
        interpreter = Interpreter(lexer)
        result = interpreter.expr()
        print(result)

if __name__ == '__main__':
    main()
将上述的代码保存到 calc6.py 文件,自己尝试一下,看下新的解释器是否可以正确地计算出带有不同运算符和括号的算术表达式。

下面是一段简单的会话:

$ python calc6.py
calc> 3
3
calc> 2 + 7 * 4
30
calc> 7 - 8 / 4
5
calc> 14 + 2 * 3 - 6 / 2
17
calc> 7 + 3 * (10 / (12 / (3 + 1) - 1))
22
calc> 7 + 3 * (10 / (12 / (3 + 1) - 1)) / (2 + 3) - 5 - 3 + (8)
10
calc> 7 + (((3 + 2)))
12
下面是今天的新练习:



像这篇文章中介绍的一样,写一个你自己的算术表达式解释器。记住:重复是学习之母。
嘿,你读到最后了!恭喜,你刚学习了如何创建(如果你完成了练习,那么你事实上编写了)一个基本的、可以计算相当复杂的算术表达式的递归下降解析器/解释器。

下一篇文章我将会谈及更多关于递归下降解析器的细节。我还会介绍一种重要的、在解释器和编译器构造中被广泛使用的数据结构,该数据结构将会贯穿后续的系列文章。

敬请期待和很快再见。直到那时,继续编写你的解释器,最重要的是:享受这个过程!

下面是我推荐的一些书籍列表,它们对你学习解释器和编译器有帮助:

Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages (Pragmatic Programmers)
Writing Compilers and Interpreters: A Software Engineering Approach
Modern Compiler Implementation in Java
Modern Compiler Design
Compilers: Principles, Techniques, and Tools (2nd Edition)

2016-2-2 23:14
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 4 楼』:  3手把手教你做一个 C 语言编译器

手把手教你做一个 C 语言编译器(0):前言
“手把手教你构建 C 语言编译器” 这一系列教程将带你从头编写一个 C 语言的编译器。希望通过这个系列,我们能对编译器的构建有一定的了解,同时,我们也将构建出一个能用的 C 语言编译器,尽管有许多语法并不支持。

在开始进入正题之前,本篇是一些闲聊,谈谈这个系列的初衷。如果你急切地想进入正篇,请跳过本章。

前言

为什么要学编译原理

如果要我说计算机专业最重要的三门课,我会说是《数据结构》、《算法》和《编译原理》。在我看来,能不能理解“递归”像是程序员的第一道门槛,而会不会写编译器则是第二道。

(当然,并不是说是没写过编译器就不是好程序员,只能说它是一个相当大的挑战吧)

以前人们会说,学习了编译原理,你就能写出更加高效的代码,但随着计算机性能的提升,代码是否高效显得就不那么重要了。那么为什么要学习编译原理呢?

原因只有一个:装B。

好吧,也许现在还想学习编译原理的人只可能是因为兴趣了。一方面想了解它的工作原理;另一方面希望挑战一下自己,看看自己能走多远。

理论很复杂,实现也很复杂?

我对编译器一直心存敬佩。所以当学校开《编译原理》的课程后,我是抱着满腔热情去上课的,但是两节课后我就放弃了。原因是太复杂了,听不懂。

一般编译原理的课程会说一些:

如何表示语法(BNF什么的)
词法分析,用什么有穷自动机和无穷自动机
语法分析,递归下降法,什么 LL(k),LALR 分析。
中间代码的表示
代码的生成
代码优化
我相信绝大多数(98%)的学生顶多学到语法分析就结束了。并且最重要的是,学了这么多也没用!依旧帮助不了我们学习编译器!这其中最主要的原因是《编译原理》试图教会我们的是如何构造“编译器生成器”,即构造一个工具,根据文法来生成编译器(如 lex/yacc)等等。

这些理论试图教会我们如何用通用的方法来自动解决问题,它们有很强的实际意义,只是对于一般的学生或程序员来说,它们过于强大,内容过于复杂。如果你尝试阅读 lex/yacc (或 flex/bison)的代码,就会发现太可怕了。

然而如果你能跟我一样,真正来实现一个简单的编译器,那么你会发现,比起可怕的《编译原理》,这点复杂度还是不算什么的(因为好多理论根本用不上)。

项目的初衷

有一次在 Github 上看到了一个项目(当时很火的),名叫 c4,号称用 4 个函数来实现了一个小的 C 语言编译器。它最让我震惊的是能够自举,即能自己编译自己。并且它用很少的代码就完成了一个功能相当完善的 C 语言编译器。

一般的编译器相关的教程要么就十分简单(如实现四则运算),要么就是借助了自动生成的工具(如 flex/bison)。而 c4 的代码完全是手工实现的,不用外部工具。可惜的是它的代码初衷是代码最小化,所以写得很乱,很难懂。所以本项目的主要目的:

实现一个功能完善的 C 语言编译器
通过教程来说明这个过程。
c4 大致500+行。重写的代码历时一周,总共代码加注释1400行。项目地址: Write a C Interpreter。

声明:本项目中的代码逻辑绝大多数取自 c4 ,但确为自己重写。

预警

在写编译器的时候会遇到两个主要问题:

麻烦,会有许多类似的代码,写起来很无聊。
难以调试,一方面没有很好的测试用例,另一方面需要对照生成的代码来调试(遇到的时候就知道了)。
所以我希望你有足够的耐心和时间来学习,相信当你真正完成的时候会像我一样,十分有成就感。

PS. 第一篇完全没有正题相关的内容也是希望你能有所心理准备再开始学习。

参考资料

最后想介绍几个资料:

Let’s Build a Compiler 很好的初学者教程,英文的。
Lemon Parser Generator,一个语法分析器生成器,对照《编译原理》观看效果更佳。
祝你学得愉快。
手把手教你做一个 C 语言编译器(1):设计
本章是“手把手教你构建 C 语言编译器”系列的第二篇,我们要从整体上讲解如何设计我们的 C 语言编译器。

本系列:

手把手教你做一个 C 语言编译器(0):前言
首先要说明的是,虽然标题是编译器,但实际上我们构建的是 C 语言的解释器,这意味着我们可以像运行脚本一样去运行 C 语言的源代码文件。这么做的理由有两点:

解释器与编译器仅在代码生成阶段有区别,而其它方面如词法分析、语法分析是一样的。
解释器需要我们实现自己的虚拟机与指令集,而这部分能帮助我们了解计算机的工作原理。
编译器的构建流程

一般而言,编译器的编写分为 3 个步骤:

词法分析器,用于将字符串转化成内部的表示结构。
语法分析器,将词法分析得到的标记流(token)生成一棵语法树。
目标代码的生成,将语法树转化成目标代码。
已经有许多工具能帮助我们处理阶段1和2,如 flex 用于词法分析,bison 用于语法分析。只是它们的功能都过于强大,屏蔽了许多实现上的细节,对于学习构建编译器帮助不大。所以我们要完全手写这些功能。

所以我们会根据下面的流程:

构建我们自己的虚拟机以及指令集。这后生成的目标代码便是我们的指令集。
构建我们的词法分析器
构建语法分析器
编译器的框架

我们的编译器主要包括 4 个函数:

next() 用于词法分析,获取下一个标记,它将自动忽略空白字符。
program() 语法分析的入口,分析整个 C 语言程序。
expression(level) 用于解析一个表达式。
eval() 虚拟机的入口,用于解释目标代码。
这里有一个单独用于解析“表达式”的函数 expression 是因为表达式在语法分析中相对独立并且比较复杂,所以我们将它单独作为一个模块(函数)。

因为我们的源代码看起来就像是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <string.h>

int token;            
// current token
char *src, *old_src;  
// pointer to source code string;
int poolsize;         
// default size of text/data/stack
int line;            
// line number

void next() {
    token = *src++;
    return;
}

void expression(int level) {
   
// do nothing
}

void program() {
    next();                  
// get next token
    while (token > 0) {
        printf("token is: %c\n", token);
        next();
    }
}

int eval() {
// do nothing yet
    return 0;
}

int main(int argc, char **argv)
{
    int i, fd;

    argc--;
    argv++;

    poolsize = 256 * 1024;
// arbitrary size
    line = 1;

    if ((fd = open(*argv, 0)) < 0) {
        printf("could not open(%s)\n", *argv);
        return -1;
    }

    if (!(src = old_src = malloc(poolsize))) {
        printf("could not malloc(%d) for source area\n", poolsize);
        return -1;
    }

   
// read the source file
    if ((i = read(fd, src, poolsize-1)) <= 0) {
        printf("read() returned %d\n", i);
        return -1;
    }
    src[i] = 0;
// add EOF character
    close(fd);

    program();
    return eval();
}
上面的代码看上去挺复杂,但其实内容不多,就是读取一个源代码文件,逐个读取每个字符,并输出每个字符。这里重要的是注意每个函数的作用,后面的文章中,我们将逐个填充每个函数的功能,最终构建起我们的编译器。

本节的代码可以在 Github 上下载,也可以直接 clone

1
git clone -b step-0 https:
//github.com/lotabout/write-a-C-interpreter
这样我们就有了一个最简单的编译器:什么都不干的编译器,下一章中,我们将实现其中的eval函数,即我们自己的虚拟机。
手把手教你做一个 C 语言编译器(2):虚拟机
本章是“手把手教你构建 C 语言编译器”系列的第三篇,本章我们要构建一台虚拟的电脑,设计我们自己的指令集,运行我们的指令集,说得通俗一点就是自己实现一套汇编语言。它们将作为我们的编译器最终输出的目标代码。

本系列:

手把手教你做一个 C 语言编译器(0):前言
手把手教你做一个 C 语言编译器(1):设计
计算机的内部工作原理

我们关心计算机的三个基本部件:CPU、寄存器及内存。代码(汇编指令)以二进制的形式保存在内存中,CPU 从中一条条地加载指令执行。程序运行的状态保存在寄存器中。

内存

我们从内存开始说起。现代的操作系统都不直接使用内存,而是使用虚拟内存。虚拟内存可以理解为一种映射,在我们的程序眼中,我们可以使用全部的内存地址,而操作系统需要将它映射到实际的内存上。当然,这些并不重要,重要的是一般而言,进程的内存会被分成几个段:

代码段(text)用于存放代码(指令)。
数据段(data)用于存放初始化了的数据,如int i = 10;,就需要存放到数据段中。
未初始化数据段(bss)用于存放未初始化的数据,如 int i[1000];,因为不关心其中的真正数值,所以单独存放可以节省空间,减少程序的体积。
栈(stack)用于处理函数调用相关的数据,如调用帧(calling frame)或是函数的局部变量等。
堆(heap)用于为程序动态分配内存。
它们在内存中的位置类似于下图:

+------------------+
|    stack   |     |      high address
|    ...     v     |
|                  |
|                  |
|                  |
|                  |
|    ...     ^     |
|    heap    |     |
+------------------+
| bss  segment     |
+------------------+
| data segment     |
+------------------+
| text segment     |      low address
+------------------+
但我们的虚拟机并不模拟完整的计算机,我们只关心三个内容:代码段、数据段以及栈。其中的数据段我们只存放字符串,因为我们的编译器并不支持初始化变量,因此我们也不需要未初始化数据段。理论上我们的虚拟器需要维护自己的堆用于内存分配,但实际实现上较为复杂且与编译无关,故我们引入一个指令MSET,使我们能直接使用编译器(解释器)中的内存。

综上,我们需要首先在全局添加如下代码:

1
2
3
4
int *text,            
// text segment
     *old_text,        
// for dump text segment
     *stack;           
// stack
char *data;           
// data segment
注意这里的类型,虽然是int型,但理解起来应该作为无符号的整型,因为我们会在代码段(text)中存放如指针/内存地址的数据,它们就是无符号的。其中数据段(data)由于只存放字符串,所以是 char * 型的

接着,在main函数中加入初始化代码,真正为其分配内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main() {
    close(fd);
    ...

    // allocate memory for virtual machine
    if (!(text = old_text = malloc(poolsize))) {
        printf("could not malloc(%d) for text area\n", poolsize);
        return -1;
    }
    if (!(data = malloc(poolsize))) {
        printf("could not malloc(%d) for data area\n", poolsize);
        return -1;
    }
    if (!(stack = malloc(poolsize))) {
        printf("could not malloc(%d) for stack area\n", poolsize);
        return -1;
    }

    memset(text, 0, poolsize);
    memset(data, 0, poolsize);
    memset(stack, 0, poolsize);

    ...
    program();
寄存器

计算机中的寄存器用于存放计算机的运行状态,真正的计算机中有许多不同种类的寄存器,但我们的虚拟机中只使用 4 个寄存器,分别如下:

PC 程序计数器,它存放的是一个内存地址,该地址中存放着 下一条 要执行的计算机指令。
SP 指针寄存器,永远指向当前的栈顶。注意的是由于栈是位于高地址并向低地址增长的,所以入栈时 SP 的值减小。
BP 基址指针。也是用于指向栈的某些位置,在调用函数时会使用到它。
AX 通用寄存器,我们的虚拟机中,它用于存放一条指令执行后的结果。
要理解这些寄存器的作用,需要去理解程序运行中会有哪些状态。而这些寄存器只是用于保存这些状态的。

在全局中加入如下定义:

1
int *pc, *bp, *sp, ax, cycle;
// virtual machine registers
在 main 函数中加入初始化代码,注意的是PC在初始应指向目标代码中的main函数,但我们还没有写任何编译相关的代码,因此先不处理。代码如下:

1
2
3
4
5
6
7
8
memset(stack, 0, poolsize);
...

bp = sp = (int *)((int)stack + poolsize);
ax = 0;

...
program();
与 CPU 相关的是指令集,我们将专门作为一个小节。

指令集

指令集是 CPU 能识别的命令的集合,也可以说是 CPU 能理解的语言。这里我们要为我们的虚拟机构建自己的指令集。它们基于 x86 的指令集,但要更为简单。

首先在全局变量中加入一个枚举类型,这是我们要支持的全部指令:

1
2
3
4
// instructions
enum { LEA ,IMM ,JMP ,CALL,JZ  ,JNZ ,ENT ,ADJ ,LEV ,LI  ,LC  ,SI  ,SC  ,PUSH,
       OR  ,XOR ,AND ,EQ  ,NE  ,LT  ,GT  ,LE  ,GE  ,SHL ,SHR ,ADD ,SUB ,MUL ,DIV ,MOD ,
       OPEN,READ,CLOS,PRTF,MALC,MSET,MCMP,EXIT };
这些指令的顺序安排是有意的,稍后你会看到,带有参数的指令在前,没有参数的指令在后。这种顺序的唯一作用就是在打印调试信息时更加方便。但我们讲解的顺序并不依据它。

MOV

MOV 是所有指令中最基础的一个,它用于将数据放进寄存器或内存地址,有点类似于 C 语言中的赋值语句。x86 的 MOV 指令有两个参数,分别是源地址和目标地址:MOV dest, source (Intel 风格),表示将 source 的内容放在 dest 中,它们可以是一个数、寄存器或是一个内存地址。

一方面,我们的虚拟机只有一个寄存器,另一方面,识别这些参数的类型(是数还是地址)是比较困难的,因此我们将 MOV 指令拆分成 5 个指令,这些指令只接受一个参数,如下:

IMM <num> 将 <num> 放入寄存器 ax 中。
LC 将对应地址中的字符载入 ax 中,要求 ax 中存放地址。
LI 将对应地址中的整数载入 ax 中,要求 ax 中存放地址。
SC 将 ax 中的数据作为字符存放入地址中,要求栈顶存放地址。
SI 将 ax 中的数据作为整数存放入地址中,要求栈顶存放地址。
你可能会觉得将一个指令变成了许多指令,整个系统就变得复杂了,但实际情况并非如此。首先是 MOV 指令其实有许多变种,根据类型的不同有 MOVB, MOVW 等指令,我们这里的LC/SC 和 LI/SI 就是对应字符型和整型的存取操作。

但最为重要的是,通过将 MOV 指令拆分成这些指令,只有 IMM 需要有参数,且不需要判断类型,所以大大简化了实现的难度。

在 eval() 函数中加入下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
void eval() {
    int op, *tmp;
    while (1) {
        if (op == IMM)       {ax = *pc++;}                                    
// load immediate value to ax
        else if (op == LC)   {ax = *(char *)ax;}                              
// load character to ax, address in ax
        else if (op == LI)   {ax = *(int *)ax;}                                
// load integer to ax, address in ax
        else if (op == SC)   {ax = *(char *)*sp++ = ax;}                       
// save character to address, value in ax, address on stack
        else if (op == SI)   {*(int *)*sp++ = ax;}                             
// save integer to address, value in ax, address on stack
    }

    ...
    return 0;
}
其中的 *sp++ 的作用是退栈,相当于 POP 操作。

这里要解释的一点是,为什么 SI/SC 指令中,地址存放在栈中,而 LI/LC 中,地址存放在ax 中?原因是默认计算的结果是存放在 ax 中的,而地址通常是需要通过计算获得,所以执行 LI/LC 时直接从 ax 取值会更高效。另一点是我们的 PUSH 指令只能将 ax 的值放到栈上,而不能以值作为参数,详细见下文。

PUSH

在 x86 中,PUSH 的作用是将值或寄存器,而在我们的虚拟机中,它的作用是将 ax 的值放入栈中。这样做的主要原因是为了简化虚拟机的实现,并且我们也只有一个寄存器 ax 。代码如下:

1
else if (op == PUSH) {*--sp = ax;}
// push the value of ax onto the stack
JMP

JMP <addr> 是跳转指令,无条件地将当前的 PC 寄存器设置为指定的 <addr>,实现如下:

1
else if (op == JMP)  {pc = (int *)*pc;}     
// jump to the address
要记得,pc 寄存器指向的是 下一条 指令。所以此时它存放的是 JMP 指令的参数,即<addr> 的值。

JZ/JNZ

为了实现 if 语句,我们需要条件判断相关的指令。这里我们只实现两个最简单的条件判断,即结果(ax)为零或不为零情况下的跳转。

实现如下:

1
else if (op == JZ)   {pc = ax ? pc + 1 : (int *)*pc;}                  
// jump if ax is zero
1
else if (op == JNZ)  {pc = ax ? (int *)*pc : pc + 1;}                  
// jump if ax is zero
子函数调用

这是汇编中最难理解的部分,所以合在一起说,要引入的命令有 CALL, ENT, ADJ 及LEV。

首先我们介绍 CALL <addr> 与 RET 指令,CALL 的作用是跳转到地址为 <addr> 的子函数,RET 则用于从子函数中返回。

为什么不能直接使用 JMP 指令呢?原因是当我们从子函数中返回时,程序需要回到跳转之前的地方继续运行,这就需要事先将这个位置信息存储起来。反过来,子函数要返回时,就需要获取并恢复这个信息。因此实际中我们将 PC 保存在栈中。如下:

1
2
else if (op == CALL) {*--sp = (int)(pc+1); pc = (int *)*pc;}           
// call subroutine
//else if (op == RET)  {pc = (int *)*sp++;}                            // return from subroutine;
这里我们把 RET 相关的内容注释了,是因为之后我们将用 LEV 指令来代替它。

在实际调用函数时,不仅要考虑函数的地址,还要考虑如何传递参数和如何返回结果。这里我们约定,如果子函数有返回结果,那么就在返回时保存在 ax 中,它可以是一个值,也可以是一个地址。那么参数的传递呢?

各种编程语言关于如何调用子函数有不同的约定,例如 C 语言的调用标准是:

由调用者将参数入栈。
调用结束时,由调用者将参数出栈。
参数逆序入栈。
事先声明一下,我们的编译器参数是顺序入栈的,下面的例子(C 语言调用标准)取自 维基百科:

1
2
3
4
5
6
7
8
9
10
int callee(int, int, int);

int caller(void)
{
    int i, ret;

    ret = callee(1, 2, 3);
    ret += 5;
    return ret;
}
会生成如下的 x86 汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
caller:
    ; make new call frame
    push    ebp
    mov     ebp, esp
        sub     1, esp       ; save stack for variable: i
    ; push call arguments
    push    3
    push    2
    push    1
    ; call subroutine 'callee'
    call    callee
    ; remove arguments from frame
    add     esp, 12
    ; use subroutine result
    add     eax, 5
    ; restore old call frame
        mov     esp, ebp
    pop     ebp
    ; return
    ret
上面这段代码在我们自己的虚拟机里会有几个问题:

push ebp,但我们的 PUSH 指令并无法指定寄存器。
mov ebp, esp,我们的 MOV 指令同样功能不足。
add esp, 12,也是一样的问题(尽管我们还没定义)。
也就是说由于我们的指令过于简单(如只能操作ax寄存器),所以用上面提到的指令,我们连函数调用都无法实现。而我们又不希望扩充现有指令的功能,因为这样实现起来就会变得复杂,因此我们采用的方法是增加指令集。毕竟我们不是真正的计算机,增加指令会消耗许多资源(钱)。

ENT

ENT <size> 指的是 enter,用于实现 ‘make new call frame’ 的功能,即保存当前的栈指针,同时在栈上保留一定的空间,用以存放局部变量。对应的汇编代码为:

1
2
3
4
; make new call frame
push    ebp
mov     ebp, esp
       sub     1, esp       ; save stack for variable: i
实现如下:

1
else if (op == ENT)  {*--sp = (int)bp; bp = sp; sp = sp - *pc++;}      // make new stack frame
ADJ

ADJ <size> 用于实现 ‘remove arguments from frame’。在将调用子函数时压入栈中的数据清除,本质上是因为我们的 ADD 指令功能有限。对应的汇编代码为:

1
2
; remove arguments from frame
add     esp, 12
实现如下:

1
else if (op == ADJ)  {sp = sp + *pc++;}                                // add esp, <size>
LEV

本质上这个指令并不是必需的,只是我们的指令集中并没有 POP 指令。并且三条指令写来比较麻烦且浪费空间,所以用一个指令代替。对应的汇编指令为:

1
2
3
4
5
; restore old call frame
       mov     esp, ebp
pop     ebp
; return
ret
具体的实现如下:

1
else if (op == LEV)  {sp = bp; bp = (int *)*sp++; pc = (int *)*sp++;}  // restore call frame and PC
注意的是,LEV 已经把 RET 的功能包含了,所以我们不再需要 RET 指令。

LEA

上面的一些指令解决了调用帧的问题,但还有一个问题是如何在子函数中获得传入的参数。这里我们首先要了解的是当参数调用时,栈中的调用帧是什么样的。我们依旧用上面的例子(只是现在用“顺序”调用参数):

sub_function(arg1, arg2, arg3);

|    ....       | high address
+---------------+
| arg: 1        |    new_bp + 4
+---------------+
| arg: 2        |    new_bp + 3
+---------------+
| arg: 3        |    new_bp + 2
+---------------+
|return address |    new_bp + 1
+---------------+
| old BP        | <- new BP
+---------------+
| local var 1   |    new_bp - 1
+---------------+
| local var 2   |    new_bp - 2
+---------------+
|    ....       |  low address
所以为了获取第一个参数,我们需要得到 new_bp + 4,但就如上面的说,我们的 ADD 指令无法操作除 ax 外的寄存器,所以我们提供了一个新的指令:LEA <offset>

实现如下:

1
else if (op == LEA)  {ax = (int)(bp + *pc++);}                         // load address for arguments.
以上就是我们为了实现函数调用需要的指令了。

运算符指令

我们为 C 语言中支持的运算符都提供对应汇编指令。每个运算符都是二元的,即有两个参数,第一个参数放在栈顶,第二个参数放在 ax 中。这个顺序要特别注意。因为像 -,/之类的运算符是与参数顺序有关的。计算后会将栈顶的参数退栈,结果存放在寄存器 ax中。因此计算结束后,两个参数都无法取得了(汇编的意义上,存在内存地址上就另当别论)。

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
else if (op == OR)  ax = *sp++ | ax;
else if (op == XOR) ax = *sp++ ^ ax;
else if (op == AND) ax = *sp++ & ax;
else if (op == EQ)  ax = *sp++ == ax;
else if (op == NE)  ax = *sp++ != ax;
else if (op == LT)  ax = *sp++ < ax;
else if (op == LE)  ax = *sp++ <= ax;
else if (op == GT)  ax = *sp++ >  ax;
else if (op == GE)  ax = *sp++ >= ax;
else if (op == SHL) ax = *sp++ << ax;
else if (op == SHR) ax = *sp++ >> ax;
else if (op == ADD) ax = *sp++ + ax;
else if (op == SUB) ax = *sp++ - ax;
else if (op == MUL) ax = *sp++ * ax;
else if (op == DIV) ax = *sp++ / ax;
else if (op == MOD) ax = *sp++ % ax;
内置函数

程序要有用,除了核心的逻辑外还需要输入输出,如 C 语言中我们经常使用的 printf 函数就是用于输出。但是 printf 函数的实现本身就十分复杂,如果我们的编译器要达到自举,就势必要实现 printf 之类的函数,但它又与编译器没有太大的联系,因此我们继续实现新的指令,从虚拟机的角度予以支持。

编译器中我们需要用到的函数有:exit, open, close, read, printf, malloc, memset 及memcmp。代码如下:

1
2
3
4
5
6
7
8
else if (op == EXIT) { printf("exit(%d)", *sp); return *sp;}
else if (op == OPEN) { ax = open((char *)sp[1], sp[0]); }
else if (op == CLOS) { ax = close(*sp);}
else if (op == READ) { ax = read(sp[2], (char *)sp[1], *sp); }
else if (op == PRTF) { tmp = sp + pc[1]; ax = printf((char *)tmp[-1], tmp[-2], tmp[-3], tmp[-4], tmp[-5], tmp[-6]); }
else if (op == MALC) { ax = (int)malloc(*sp);}
else if (op == MSET) { ax = (int)memset((char *)sp[2], sp[1], *sp);}
else if (op == MCMP) { ax = memcmp((char *)sp[2], (char *)sp[1], *sp);}
这里的原理是,我们的电脑上已经有了这些函数的实现,因此编译编译器时,这些函数的二进制代码就被编译进了我们的编译器,因此在我们的编译器/虚拟机上运行我们提供的这些指令时,这些函数就是可用的。换句话说就是不需要我们自己去实现了。

最后再加上一个错误判断:

1
2
3
4
else {
    printf("unknown instruction:%d\n", op);
    return -1;
}
测试

下面我们用我们的汇编写一小段程序,来计算 10+20,在 main 函数中加入下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(int argc, char *argv[])
{
    ax = 0;
    ...

    i = 0;
    text[i++] = IMM;
    text[i++] = 10;
    text[i++] = PUSH;
    text[i++] = IMM;
    text[i++] = 20;
    text[i++] = ADD;
    text[i++] = PUSH;
    text[i++] = EXIT;
    pc = text;

    ...
    program();
}
编译程序 gcc xc-tutor.c,运行程序:./a.out hello.c。输出

1
exit(30)
注意我们的之前的程序需要指令一个源文件,只是现在还用不着,但从结果可以看出,我们的虚拟机还是工作良好的。

小结

本章中我们回顾了计算机的内部运行原理,并仿照 x86 汇编指令设计并实现了我们自己的指令集。

本章的代码可以在 Github 上下载,也可以直接 clone

1
git clone -b step-1 [url]https://github.com/lotabout/write-a-C-interpreter[/url]
实际计算机中,添加一个新的指令需要设计许多新的电路,会增加许多的成本,但我们的需要机中,新的指令几乎不消耗资源,因此我们可以利用这一点,用更多的指令来完成更多的功能,从而简化具体的实现。
手把手教你做一个 C 语言编译器(3):词法分析器
本章我们要讲解如何构建词法分析器。

本系列:

手把手教你做一个 C 语言编译器(0):前言
手把手教你做一个 C 语言编译器(1):设计
手把手教你做一个 C 语言编译器(2):虚拟机
什么是词法分析器

简而言之,词法分析器用于对源码字符串做预处理,以减少语法分析器的复杂程度。

词法分析器以源码字符串为输入,输出为标记流(token stream),即一连串的标记,每个标记通常包括: (token, token value) 即标记本身和标记的值。例如,源码中若包含一个数字 '998' ,词法分析器将输出 (Number, 998),即(数字,998)。再例如:

1
2
3
2 + 3 * (4 - 5)
=>
(Number, 2) Add (Number, 3) Multiply Left-Bracket (Number, 4) Subtract (Number, 5) Right-Bracket
通过词法分析器的预处理,语法分析器的复杂度会大大降低,这点在后面的语法分析器我们就能体会。

词法分析器与编译器

要是深入词法分析器,你就会发现,它的本质上也是编译器。我们的编译器是以标记流为输入,输出汇编代码,而词法分析器则是以源码字符串为输入,输出标记流。

                   +-------+                      +--------+
-- source code --> | lexer | --> token stream --> | parser | --> assembly
                   +-------+                      +--------+
在这个前提下,我们可以这样认为:直接从源代码编译成汇编代码是很困难的,因为输入的字符串比较难处理。所以我们先编写一个较为简单的编译器(词法分析器)来将字符串转换成标记流,而标记流对于语法分析器而言就容易处理得多了。

词法分析器的实现

由于词法分析的工作很常见,但又枯燥且容易出错,所以人们已经开发出了许多工具来生成词法分析器,如 lex, flex。这些工具允许我们通过正则表达式来识别标记。

这里注意的是,我们并不会一次性地将所有源码全部转换成标记流,原因有二:

字符串转换成标记流有时是有状态的,即与代码的上下文是有关系的。
保存所有的标记流没有意义且浪费空间。
所以实际的处理方法是提供一个函数(即前几篇中提到的 next()),每次调用该函数则返回下一个标记。

支持的标记

在全局中添加如下定义:

1
2
3
4
5
6
// tokens and classes (operators last and in precedence order)
enum {
  Num = 128, Fun, Sys, Glo, Loc, Id,
  Char, Else, Enum, If, Int, Return, Sizeof, While,
  Assign, Cond, Lor, Lan, Or, Xor, And, Eq, Ne, Lt, Gt, Le, Ge, Shl, Shr, Add, Sub, Mul, Div, Mod, Inc, Dec, Brak
};
这些就是我们要支持的标记符。例如,我们会将 = 解析为 Assign;将 == 解析为Eq;将 != 解析为 Ne 等等。

所以这里我们会有这样的印象,一个标记(token)可能包含多个字符,且多数情况下如此。而词法分析器能减小语法分析复杂度的原因,正是因为它相当于通过一定的编码(更多的标记)来压缩了源码字符串。

当然,上面这些标记是有顺序的,跟它们在 C 语言中的优先级有关,如 *(Mul) 的优先级就要高于 +(Add)。它们的具体使用在后面的语法分析中会提到。

最后要注意的是还有一些字符,它们自己就构成了标记,如右方括号 ] 或波浪号 ~等。我们不另外处理它们的原因是:

它们是单字符的,即并不是多个字符共同构成标记(如 == 需要两个字符);
它们不涉及优先级关系。
词法分析器的框架

即 next() 函数的主体:

1
2
3
4
5
6
7
8
9
10
void next() {
    char *last_pos;
    int hash;

    while (token = *src) {
        ++src;
        
// parse token here
    }
    return;
}
这里的一个问题是,为什么要用 while 循环呢?这就涉及到编译器(记得我们说过词法分析器也是某种意义上的编译器)的一个问题:如何处理错误?

对词法分析器而言,若碰到了一个我们不认识的字符该怎么处理?一般处理的方法有两种:

指出错误发生的位置,并退出整个程序
指出错误发生的位置,跳过当前错误并继续编译
这个 while 循环的作用就是跳过这些我们不识别的字符,我们同时还用它来处理空白字符。我们知道,C 语言中空格是用来作为分隔用的,并不作为语法的一部分。因此在实现中我们将它作为“不识别”的字符,这个 while 循环可以用来跳过它。

换行符

换行符和空格类似,但有一点不同,每次遇到换行符,我们需要将当前的行号加一:

1
2
3
4
5
6
7
// parse token here
...

if (token == '\n') {
    ++line;
}
...
宏定义

C 语言的宏定义以字符 # 开头,如 # include <stdio.h>。我们的编译器并不支持宏定义,所以直接跳过它们。

1
2
3
4
5
6
else if (token == '#') {
   
// skip macro, because we will not support it
    while (*src != 0 && *src != '\n') {
        src++;
    }
}
标识符与符号表

标识符(identifier)可以理解为变量名。对于语法分析而言,我们并不关心一个变量具体叫什么名字,而只关心这个变量名代表的唯一标识。例如 int a; 定义了变量a,而之后的语句 a = 10,我们需要知道这两个 a 指向的是同一个变量。

基于这个理由,词法分析器会把扫描到的标识符全都保存到一张表中,遇到新的标识符就去查这张表,如果标识符已经存在,就返回它的唯一标识。

那么我们怎么表示标识符呢?如下:

1
2
3
4
5
6
7
8
9
10
11
struct identifier {
    int token;
    int hash;
    char * name;
    int class;
    int type;
    int value;
    int Bclass;
    int Btype;
    int Bvalue;
}
这里解释一下具体的含义:

token:该标识符返回的标记,理论上所有的变量返回的标记都应该是 Id,但实际上由于我们还将在符号表中加入关键字如 if, while 等,它们都有对应的标记。
hash:顾名思义,就是这个标识符的哈希值,用于标识符的快速比较。
name:存放标识符本身的字符串。
class:该标识符的类别,如数字,全局变量或局部变量等。
type:标识符的类型,即如果它是个变量,变量是 int 型、char 型还是指针型。
value:存放这个标识符的值,如标识符是函数,刚存放函数的地址。
BXXXX:C 语言中标识符可以是全局的也可以是局部的,当局部标识符的名字与全局标识符相同时,用作保存全局标识符的信息。
由上可以看出,我们实现的词法分析器与传统意义上的词法分析器不太相同。传统意义上的符号表只需要知道标识符的唯一标识即可,而我们还存放了一些只有语法分析器才会得到的信息,如 type 。

由于我们的目标是能自举,而我们定义的语法不支持 struct,故而使用下列方式。

Symbol table:
----+-----+----+----+----+-----+-----+-----+------+------+----
.. |token|hash|name|type|class|value|btype|bclass|bvalue| ..
----+-----+----+----+----+-----+-----+-----+------+------+----
    |<---       one single identifier                --->|
即用一个整型数组来保存相关的ID信息。每个ID占用数组中的9个空间,分析标识符的相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int token_val;               
// value of current token (mainly for number)
int *current_id,              
// current parsed ID
    *symbols;                 
// symbol table

// fields of identifier
enum {Token, Hash, Name, Type, Class, Value, BType, BClass, BValue, IdSize};

void next() {
        ...

        else if ((token >= 'a' && token <= 'z') || (token >= 'A' && token <= 'Z') || (token == '_')) {

            
// parse identifier
            last_pos = src - 1;
            hash = token;

            while ((*src >= 'a' && *src <= 'z') || (*src >= 'A' && *src <= 'Z') || (*src >= '0' && *src <= '9') || (*src == '_')) {
                hash = hash * 147 + *src;
                src++;
            }

            
// look for existing identifier, linear search
            current_id = symbols;
            while (current_id[Token]) {
                if (current_id[Hash] == hash && !memcmp((char *)current_id[Name], last_pos, src - last_pos)) {
                    
//found one, return
                    token = current_id[Token];
                    return;
                }
                current_id = current_id + IdSize;
            }

            
// store new ID
            current_id[Name] = (int)last_pos;
            current_id[Hash] = hash;
            token = current_id[Token] = Id;
            return;
        }
        ...
}
查找已有标识符的方法是线性查找 symbols 表。

数字

数字中较为复杂的一点是需要支持十进制、十六进制及八进制。逻辑也较为直接,可能唯一不好理解的是获取十六进制的值相关的代码。

1
token_val = token_val * 16 + (token & 16) + (token >= 'A' ? 9 : 0);
这里要注意的是在ASCII码中,字符a对应的十六进制值是 61, A是41,故通过(token & 16) 可以得到个位数的值。其它就不多说了,这里这样写的目的是装B(其实是抄 c4 的源代码的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void next() {
        ...

        else if (token >= '0' && token <= '9') {
            
// parse number, three kinds: dec(123) hex(0x123) oct(017)
            token_val = token - '0';
            if (token_val) {
                if (*src == 'x' || *src == 'X') {
                    
//hex
                    token = *++src;
                    while ((token >= '0' && token <= '9') || (token >= 'a' && token <= 'f') || (token >= 'A' && token <= 'F')) {
                        token_val = token_val * 16 + (token & 16) + (token >= 'A' ? 9 : 0);
                        token = *++src;
                    }
                } else {
                    
// dec
                    while (*src >= '0' && *src <= '9') {
                        token_val = token_val*10 + *src++ - '0';
                    }
                }
            } else {
               
// oct
                while (*src >= '0' && *src <= '7') {
                    token_val = token_val*8 + *src++ - '0';
                }
            }

            token = Num;
            return;
        }

        ...
}
字符串

在分析时,如果分析到字符串,我们需要将它存放到前一篇文章中说的 data 段中。然后返回它在 data 段中的地址。另一个特殊的地方是我们需要支持转义符。例如用\n 表示换行符。由于本编译器的目的是达到自己编译自己,所以代码中并没有支持除\n 的转义符,如 \t, \r 等,但仍支持 \a 表示字符 a 的语法,如 \" 表示 "。

在分析时,我们将同时分析单个字符如 'a' 和字符串如 "a string"。若得到的是单个字符,我们以 Num 的形式返回。相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void next() {
        ...

        else if (token == '"' || token == '\'') {
            
// parse string literal, currently, the only supported escape
            
// character is '\n', store the string literal into data.
            last_pos = data;
            while (*src != 0 && *src != token) {
                token_val = *src++;
                if (token_val == '\\') {
                    
// escape character
                    token_val = *src++;
                    if (token_val == 'n') {
                        token_val = '\n';
                    }
                }

                if (token == '"') {
                    *data++ = token_val;
                }
            }

            src++;
            
// if it is a single character, return Num token
            if (token == '"') {
                token_val = (int)last_pos;
            } else {
                token = Num;
            }

            return;
        }
}
注释

在我们的 C 语言中,只支持 // 类型的注释,不支持 /* comments */ 的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void next() {
        ...

        else if (token == '/') {
            if (*src == '/') {
               
// skip comments
                while (*src != 0 && *src != '\n') {
                    ++src;
                }
            } else {
               
// divide operator
                token = Div;
                return;
            }
        }

        ...
}
这里我们要额外介绍 lookahead 的概念,即提前看多个字符。上述代码中我们看到,除了跳过注释,我们还可能返回除号 /(Div) 标记。

提前看字符的原理是:有一个或多个标记是以同样的字符开头的(如本小节中的注释与除号),因此只凭当前的字符我们并无法确定具体应该解释成哪一个标记,所以只能再向前查看字符,如本例需向前查看一个字符,若是 / 则说明是注释,反之则是除号。

我们之前说过,词法分析器本质上也是编译器,其实提前看字符的概念也存在于编译器,只是这时就是提前看k个“标记”而不是“字符”了。平时听到的 LL(k) 中的 k 就是需要向前看的标记的个数了。

另外,我们用词法分析器将源码转换成标记流,能减小语法分析复杂度,原因之一就是减少了语法分析器需要“向前看”的字符个数。

其它

其它的标记的解析就相对容易一些了,我们直接贴上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
void next() {
        ...

        else if (token == '=') {
            
// parse '==' and '='
            if (*src == '=') {
                src ++;
                token = Eq;
            } else {
                token = Assign;
            }
            return;
        }
        else if (token == '+') {
            
// parse '+' and '++'
            if (*src == '+') {
                src ++;
                token = Inc;
            } else {
                token = Add;
            }
            return;
        }
        else if (token == '-') {
            
// parse '-' and '--'
            if (*src == '-') {
                src ++;
                token = Dec;
            } else {
                token = Sub;
            }
            return;
        }
        else if (token == '!') {
            
// parse '!='
            if (*src == '=') {
                src++;
                token = Ne;
            }
            return;
        }
        else if (token == '<') {
            
// parse '<=', '<<' or '<'
            if (*src == '=') {
                src ++;
                token = Le;
            } else if (*src == '<') {
                src ++;
                token = Shl;
            } else {
                token = Lt;
            }
            return;
        }
        else if (token == '>') {
            
// parse '>=', '>>' or '>'
            if (*src == '=') {
                src ++;
                token = Ge;
            } else if (*src == '>') {
                src ++;
                token = Shr;
            } else {
                token = Gt;
            }
            return;
        }
        else if (token == '|') {
            
// parse '|' or '||'
            if (*src == '|') {
                src ++;
                token = Lor;
            } else {
                token = Or;
            }
            return;
        }
        else if (token == '&') {
            
// parse '&' and '&&'
            if (*src == '&') {
                src ++;
                token = Lan;
            } else {
                token = And;
            }
            return;
        }
        else if (token == '^') {
            token = Xor;
            return;
        }
        else if (token == '%') {
            token = Mod;
            return;
        }
        else if (token == '*') {
            token = Mul;
            return;
        }
        else if (token == '[') {
            token = Brak;
            return;
        }
        else if (token == '?') {
            token = Cond;
            return;
        }
        else if (token == '~' || token == ';' || token == '{' || token == '}' || token == '(' || token == ')' || token == ']' || token == ',' || token == ':') {
            
// directly return the character as token;
            return;
        }

        ...
}
代码较多,但主要逻辑就是向前看一个字符来确定真正的标记。

关键字与内置函数

虽然上面写完了词法分析器,但还有一个问题需要考虑,那就是“关键字”,例如 if,while, return 等。它们不能被作为普通的标识符,因为有特殊的含义。

一般有两种处理方法:

词法分析器中直接解析这些关键字。
在语法分析前将关键字提前加入符号表。
这里我们就采用第二种方法,将它们加入符号表,并提前为它们赋予必要的信息(还记得前面说的标识符 Token 字段吗?)。这样当源代码中出现关键字时,它们会被解析成标识符,但由于符号表中已经有了相关的信息,我们就能知道它们是特殊的关键字。

内置函数的行为也和关键字类似,不同的只是赋值的信息,在main函数中进行初始化如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// types of variable/function
enum { CHAR, INT, PTR };
int *idmain;                  
// the `main` function

void main() {
    ...

    src = "char else enum if int return sizeof while "
          "open read close printf malloc memset memcmp exit void main";

     
// add keywords to symbol table
    i = Char;
    while (i <= While) {
        next();
        current_id[Token] = i++;
    }

   
// add library to symbol table
    i = OPEN;
    while (i <= EXIT) {
        next();
        current_id[Class] = Sys;
        current_id[Type] = INT;
        current_id[Value] = i++;
    }

    next(); current_id[Token] = Char;
// handle void type
    next(); idmain = current_id;
// keep track of main

    ...
    program();
}
代码

本章的代码可以在 Github 上下载,也可以直接 clone

1
git clone -b step-2 https:
//github.com/lotabout/write-a-C-interpreter
上面的代码运行后会出现 ‘Segmentation Falt’,这是正常的,因为它会尝试运行我们上一章创建的虚拟机,但其中并没有任何汇编代码。

小结

本章我们为我们的编译器构建了词法分析器,通过本章的学习,我认为有几个要点需要强调:

词法分析器的作用是对源码字符串进行预处理,作用是减小语法分析器的复杂程度。
词法分析器本身可以认为是一个编译器,输入是源码,输出是标记流。
lookahead(k) 的概念,即向前看 k 个字符或标记。
词法分析中如何处理标识符与符号表。
下一章中,我们将介绍递归下降的语法分析器。我们下一章见。
手把手教你做一个 C 语言编译器(4):递归下降
本章我们将讲解递归下降的方法,并用它完成一个基本的四则运算的语法分析器。

本系列:

手把手教你做一个 C 语言编译器(0):前言
手把手教你做一个 C 语言编译器(1):设计
手把手教你做一个 C 语言编译器(2):虚拟机
手把手教你做一个 C 语言编译器(3):词法分析器
什么是递归下降

传统上,编写语法分析器有两种方法,一种是自顶向下,一种是自底自上。自顶向下是从起始非终结符开始,不断地对非终结符进行分解,直到匹配输入的终结符;自底向上是不断地将终结符进行合并,直到合并成起始的非终结符。

其中的自顶向下方法就是我们所说的递归下降。

终结符与非终结符

没有学过编译原理的话可能并不知道什么是“终结符”,“非终结符”。这里我简单介绍一下。首先是 BNF 范式,就是一种用来描述语法的语言,例如,四则运算的规则可以表示如下:

<expr> ::= <expr> + <term>
         | <expr> - <term>
         | <term>

<term> ::= <term> * <factor>
         | <term> / <factor>
         | <factor>

<factor> ::= ( <expr> )
           | Num
用尖括号 <> 括起来的就称作 非终结符 ,因为它们可以用 ::= 右侧的式子代替。| 表示选择,如 <expr> 可以是 <expr> + <term>、<expr> - <term>或 <term> 中的一种。而没有出现在::=左边的就称作 终结符 ,一般终结符对应于词法分析器输出的标记。

四则运算的递归下降

例如,我们对 3 * (4 + 2) 进行语法分析。我们假设词法分析器已经正确地将其中的数字识别成了标记 Num。

递归下降是从起始的非终结符开始(顶),本例中是 <expr>,实际中可以自己指定,不指定的话一般认为是第一个出现的非终结符。

1. <expr> => <expr>
2.           => <term>        * <factor>
3.              => <factor>     |
4.                 => Num (3)   |
5.                              => ( <expr> )
6.                                   => <expr>           + <term>
7.                                      => <term>          |
8.                                         => <factor>     |
9.                                            => Num (4)   |
10.                                                        => <factor>
11.                                                           => Num (2)
可以看到,整个解析的过程是在不断对非终结符进行替换(向下),直到遇见了终结符(底)。而我们可以从解析的过程中看出,一些非终结符如<expr>被递归地使用了。

为什么选择递归下降

从上小节对四则运算的递归下降解析可以看出,整个解析的过程和语法的 BNF 表示是二分接近的,更为重要的是,我们可以很容易地直接将 BNF 表示转换成实际的代码。方法是为每个产生式(即 非终结符 ::= ...)生成一个同名的函数。

这里会有一个疑问,就是上例中,当一个终结符有多个选择时,如何确定具体选择哪一个?如为什么用 <expr> ::= <term> * <factor> 而不是 <expr> ::= <term> / <factor> ?这就用到了上一章中提到的“向前看 k 个标记”的概念了。我们向前看一个标记,发现是 *,而这个标记足够让我们确定用哪个表达式了。

另外,递归下下降方法对 BNF 方法本身有一定的要求,否则会有一些问题,如经典的“左递归”问题。

左递归

原则上我们是不讲这么深入,但我们上面的四则运算的文法就是左递归的,而左递归的语法是没法直接使用递归下降的方法实现的。因此我们要消除左递归,消除后的文法如下:

1
2
3
4
5
6
7
8
9
10
11
12
<expr> ::= <term> <expr_tail>
<expr_tail> ::= + <term> <expr_tail>
              | - <term> <expr_tail>
              | <empty>

<term> ::= <factor> <term_tail>
<term_tail> ::= * <factor> <term_tail>
              | / <factor> <term_tail>
              | <empty>

<factor> ::= ( <expr> )
           | Num
消除左递归的相关方法,这里不再多说,请自行查阅相关的资料。

四则运算的实现

本节中我们专注语法分析器部分的实现,具体实现很容易,我们直接贴上代码,就是上述的消除左递归后的文法直接转换而来的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int expr();

int factor() {
    int value = 0;
    if (token == '(') {
        match('(');
        value = expr();
        match(')');
    } else {
        value = token_val;
        match(Num);
    }
    return value;
}

int term_tail(int lvalue) {
    if (token == '*') {
        match('*');
        int value = lvalue * factor();
        return term_tail(value);
    } else if (token == '/') {
        match('/');
        int value = lvalue / factor();
        return term_tail(value);
    } else {
        return lvalue;
    }
}

int term() {
    int lvalue = factor();
    return term_tail(lvalue);
}

int expr_tail(int lvalue) {
    if (token == '+') {
        match('+');
        int value = lvalue + term();
        return expr_tail(value);
    } else if (token == '-') {
        match('-');
        int value = lvalue - term();
        return expr_tail(value);
    } else {
        return lvalue;
    }
}

int expr() {
    int lvalue = term();
    return expr_tail(lvalue);
}
可以看到,有了BNF方法后,采用递归向下的方法来实现编译器是很直观的。

我们把词法分析器的代码一并贴上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h>

enum {Num};
int token;
int token_val;
char *line = NULL;
char *src = NULL;

void next() {
   
// skip white space
    while (*src == ' ' || *src == '\t') {
        src ++;
    }

    token = *src++;

    if (token >= '0' && token <= '9' ) {
        token_val = token - '0';
        token = Num;

        while (*src >= '0' && *src <= '9') {
            token_val = token_val*10 + *src - '0';
            src ++;
        }
        return;
    }
}

void match(int tk) {
    if (token != tk) {
        printf("expected token: %d(%c), got: %d(%c)\n", tk, tk, token, token);
        exit(-1);
    }
    next();
}
最后是main函数:

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char *argv[])
{
    size_t linecap = 0;
    ssize_t linelen;
    while ((linelen = getline(&line, &linecap, stdin)) > 0) {
        src = line;
        next();
        printf("%d\n", expr());
    }
    return 0;
}
小结

本章中我们介绍了递归下降的方法,并用它来实现了四则运算的语法分析器。

花这么大精力讲解递归下降方法,是因为几乎所有手工编写的语法分析器都或多或少地有它的影子。换句话说,掌握了递归下降的方法,就可以应付大多数的语法分析器编写。

同时我们也用实例看到了理论(BNF 语法,左递归的消除)是如何帮助我们的工程实现的。尽管理论不是必需的,但如果能掌握它,对于提高我们的水平还是很有帮助的。
手把手教你做一个 C 语言编译器(5):变量定义
本章中我们用 EBNF 来大致描述我们实现的 C 语言的文法,并实现其中解析变量定义部分。

由于语法分析本身比较复杂,所以我们将它拆分成 3 个部分进行讲解,分别是:变量定义、函数定义、表达式。

本系列:

手把手教你做一个 C 语言编译器(0):前言
手把手教你做一个 C 语言编译器(1):设计
手把手教你做一个 C 语言编译器(2):虚拟机
手把手教你做一个 C 语言编译器(3):词法分析器
手把手教你做一个 C 语言编译器(4):递归下降
EBNF 表示

EBNF 是对前一章提到的 BNF 的扩展,它的语法更容易理解,实现起来也更直观。但真正看起来还是很烦,如果不想看可以跳过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
program ::= {global_declaration}+

global_declaration ::= enum_decl | variable_decl | function_decl

enum_decl ::= 'enum' [id] '{' id ['=' 'num'] {',' id ['=' 'num'} '}'

variable_decl ::= type {'*'} id { ',' {'*'} id } ';'

function_decl ::= type {'*'} id '(' parameter_decl ')' '{' body_decl '}'

parameter_decl ::= type {'*'} id {',' type {'*'} id}

body_decl ::= {variable_decl}, {statement}

statement ::= non_empty_statement | empty_statement

non_empty_statement ::= if_statement | while_statement | '{' statement '}'
                     | 'return' expression | expression ';'

if_statement ::= 'if' '(' expression ')' statement ['else' non_empty_statement]

while_statement ::= 'while' '(' expression ')' non_empty_statement
其中 expression 相关的内容我们放到后面解释,主要原因是我们的语言不支持跨函数递归,而为了实现自举,实际上我们也不能使用递归(亏我们说了一章的递归下降)。

P.S. 我是先写程序再总结上面的文法,所以实际上它们间的对应关系并不是特别明显。

解析变量的定义

本章要讲解的就是上节文法中的 enum_decl 和 variable_decl 部分。

program()

首先是之前定义过的 program 函数,将它改成:

1
2
3
4
5
6
7
void program() {
   
// get next token
    next();
    while (token > 0) {
        global_declaration();
    }
}
我知道 global_declaration 函数还没有出现过,但没有关系,采用自顶向下的编写方法就是要不断地实现我们需要的内容。下面是 global_declaration 函数的内容:

global_declaration()

即全局的定义语句,包括变量定义,类型定义(只支持枚举)及函数定义。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
int basetype;   
// the type of a declaration, make it global for convenience
int expr_type;   
// the type of an expression

void global_declaration() {
   
// global_declaration ::= enum_decl | variable_decl | function_decl
   
//
   
// enum_decl ::= 'enum' [id] '{' id ['=' 'num'] {',' id ['=' 'num'} '}'
   
//
   
// variable_decl ::= type {'*'} id { ',' {'*'} id } ';'
   
//
   
// function_decl ::= type {'*'} id '(' parameter_decl ')' '{' body_decl '}'

    int type;
// tmp, actual type for variable
    int i;
// tmp

    basetype = INT;

   
// parse enum, this should be treated alone.
    if (token == Enum) {
        
// enum [id] { a = 10, b = 20, ... }
        match(Enum);
        if (token != '{') {
            match(Id);
// skip the [id] part
        }
        if (token == '{') {
            
// parse the assign part
            match('{');
            enum_declaration();
            match('}');
        }

        match(';');
        return;
    }

   
// parse type information
    if (token == Int) {
        match(Int);
    }
    else if (token == Char) {
        match(Char);
        basetype = CHAR;
    }

   
// parse the comma seperated variable declaration.
    while (token != ';' && token != '}') {
        type = basetype;
        
// parse pointer type, note that there may exist `int ****x;`
        while (token == Mul) {
            match(Mul);
            type = type + PTR;
        }

        if (token != Id) {
            
// invalid declaration
            printf("%d: bad global declaration\n", line);
            exit(-1);
        }
        if (current_id[Class]) {
            
// identifier exists
            printf("%d: duplicate global declaration\n", line);
            exit(-1);
        }
        match(Id);
        current_id[Type] = type;

        if (token == '(') {
            current_id[Class] = Fun;
            current_id[Value] = (int)(text + 1);
// the memory address of function
            function_declaration();
        } else {
            
// variable declaration
            current_id[Class] = Glo;
// global variable
            current_id[Value] = (int)data;
// assign memory address
            data = data + sizeof(int);
        }

        if (token == ',') {
            match(',');
        }
    }
    next();
}
看了上面的代码,能大概理解吗?这里我们讲解其中的一些细节。

向前看标记 :其中的 if (token == xxx) 语句就是用来向前查看标记以确定使用哪一个产生式,例如只要遇到 enum 我们就知道是需要解析枚举类型。而如果只解析到类型,如 int identifier 时我们并不能确定 identifier 是一个普通的变量还是一个函数,所以还需要继续查看后续的标记,如果遇到 ( 则可以断定是函数了,反之则是变量。

变量类型的表示 :我们的编译器支持指针类型,那意味着也支持指针的指针,如 int **data;。那么我们如何表示指针类型呢?前文中我们定义了支持的类型:

1
2
// types of variable/function
enum { CHAR, INT, PTR };
所以一个类型首先有基本类型,如 CHAR 或 INT,当它是一个指向基本类型的指针时,如int *data,我们就将它的类型加上 PTR 即代码中的:type = type + PTR;。同理,如果是指针的指针,则再加上 PTR。

enum_declaration()

用于解析枚举类型的定义。主要的逻辑用于解析用逗号(,)分隔的变量,值得注意的是在编译器中如何保存枚举变量的信息。

即我们将该变量的类别设置成了 Num,这样它就成了全局的常量了,而注意到上节中,正常的全局变量的类别则是 Glo,类别信息在后面章节中解析 expression 会使用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void enum_declaration() {
   
// parse enum [id] { a = 1, b = 3, ...}
    int i;
    i = 0;
    while (token != '}') {
        if (token != Id) {
            printf("%d: bad enum identifier %d\n", line, token);
            exit(-1);
        }
        next();
        if (token == Assign) {
            
// like {a=10}
            next();
            if (token != Num) {
                printf("%d: bad enum initializer\n", line);
                exit(-1);
            }
            i = token_val;
            next();
        }

        current_id[Class] = Num;
        current_id[Type] = INT;
        current_id[Value] = i++;

        if (token == ',') {
            next();
        }
    }
}
其它

其中的 function_declaration 函数我们将放到下一章中讲解。match 函数是一个辅助函数:

1
2
3
4
5
6
7
8
void match(int tk) {
    if (token == tk) {
        next();
    } else {
        printf("%d: expected token: %d\n", line, tk);
        exit(-1);
    }
}
它将 next 函数包装起来,如果不是预期的标记则报错并退出。

代码

本章的代码可以在 Github 上下载,也可以直接 clone

1
git clone -b step-3 https:
//github.com/lotabout/write-a-C-interpreter
本章的代码还无法正常运行,因为还有许多功能没有实现,但如果有兴趣的话,可以自己先试着去实现它。

小结

本章的内容应该不难,除了开头的 EBNF 表达式可能相对不好理解一些,但如果你查看了 EBNF 的具体表示方法后就不难理解了。

剩下的内容就是按部就班地将 EBNF 的产生式转换成函数的过程,如果你理解了上一章中的内容,相信这部分也不难理解。

下一章中我们将介绍如何解析函数的定义,敬请期待。
手把手教你做一个 C 语言编译器(6):函数定义
由于语法分析本身比较复杂,所以我们将它拆分成 3 个部分进行讲解,分别是:变量定义、函数定义、表达式。本章讲解函数定义相关的内容。

本系列:

手把手教你做一个 C 语言编译器(0):前言
手把手教你做一个 C 语言编译器(1):设计
手把手教你做一个 C 语言编译器(2):虚拟机
手把手教你做一个 C 语言编译器(3):词法分析器
手把手教你做一个 C 语言编译器(4):递归下降
手把手教你做一个 C 语言编译器(5):变量定义
EBNF 表示

这是上一章的 EBNF 方法中与函数定义相关的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
variable_decl ::= type {'*'} id { ',' {'*'} id } ';'

function_decl ::= type {'*'} id '(' parameter_decl ')' '{' body_decl '}'

parameter_decl ::= type {'*'} id {',' type {'*'} id}

body_decl ::= {variable_decl}, {statement}

statement ::= non_empty_statement | empty_statement

non_empty_statement ::= if_statement | while_statement | '{' statement '}'
                     | 'return' expression | expression ';'

if_statement ::= 'if' '(' expression ')' statement ['else' non_empty_statement]

while_statement ::= 'while' '(' expression ')' non_empty_statement
解析函数的定义

上一章的代码中,我们已经知道了什么时候开始解析函数的定义,相关的代码如下:

1
2
3
4
5
6
7
...
if (token == '(') {
    current_id[Class] = Fun;
    current_id[Value] = (int)(text + 1);
// the memory address of function
    function_declaration();
} else {
...
即在这断代码之前,我们已经为当前的标识符(identifier)设置了正确的类型,上面这断代码为当前的标识符设置了正确的类别(Fun),以及该函数在代码段(text segment)中的位置。接下来开始解析函数定义相关的内容:parameter_decl 及 body_decl。

函数参数与汇编代码

现在我们要回忆如何将“函数”转换成对应的汇编代码,因为这决定了在解析时我们需要哪些相关的信息。考虑下列函数:

1
2
3
4
5
6
int demo(int param_a, int *param_b) {
    int local_1;
    char local_2;

    ...
}
那么它应该被转换成什么样的汇编代码呢?在思考这个问题之前,我们需要了解当 demo函数被调用时,计算机的栈的状态,如下(参照第三章讲解的虚拟机):

|    ....       | high address
+---------------+
| arg: param_a  |    new_bp + 3
+---------------+
| arg: param_b  |    new_bp + 2
+---------------+
|return address |    new_bp + 1
+---------------+
| old BP        | <- new BP
+---------------+
| local_1       |    new_bp - 1
+---------------+
| local_2       |    new_bp - 2
+---------------+
|    ....       |  low address
这里最为重要的一点是,无论是函数的参数(如 param_a)还是函数的局部变量(如local_1)都是存放在计算机的 栈 上的。因此,与存放在 数据段 中的全局变量不同,在函数内访问它们是通过 new_bp 指针和对应的位移量进行的。因此,在解析的过程中,我们需要知道参数的个数,各个参数的位移量。

函数定义的解析

这相当于是整个函数定义的语法解析的框架,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void function_declaration() {
   
// type func_name (...) {...}
   
//               | this part

    match('(');
    function_parameter();
    match(')');
    match('{');
    function_body();
   
//match('}');                 //  ①

   
// ②
   
// unwind local variable declarations for all local variables.
    current_id = symbols;
    while (current_id[Token]) {
        if (current_id[Class] == Loc) {
            current_id[Class] = current_id[BClass];
            current_id[Type]  = current_id[BType];
            current_id[Value] = current_id[BValue];
        }
        current_id = current_id + IdSize;
    }
}
其中①中我们没有消耗最后的}字符。这么做的原因是:variable_decl 与 function_decl是放在一起解析的,而 variable_decl 是以字符 ; 结束的。而 function_decl 是以字符 }结束的,若在此通过 match 消耗了 ‘;’ 字符,那么外层的 while 循环就没法准确地知道函数定义已经结束。所以我们将结束符的解析放在了外层的 while 循环中。

而②中的代码是用于将符号表中的信息恢复成全局的信息。这是因为,局部变量是可以和全局变量同名的,一旦同名,在函数体内局部变量就会覆盖全局变量,出了函数体,全局变量就恢复了原先的作用。这段代码线性地遍历所有标识符,并将保存在 BXXX 中的信息还原。

解析参数

1
parameter_decl ::= type {'*'} id {',' type {'*'} id}
解析函数的参数就是解析以逗号分隔的一个个标识符,同时记录它们的位置与类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int index_of_bp;
// index of bp pointer on stack

void function_parameter() {
    int type;
    int params;
    params = 0;
    while (token != ')') {
        
// ①

        
// int name, ...
        type = INT;
        if (token == Int) {
            match(Int);
        } else if (token == Char) {
            type = CHAR;
            match(Char);
        }

        
// pointer type
        while (token == Mul) {
            match(Mul);
            type = type + PTR;
        }

        
// parameter name
        if (token != Id) {
            printf("%d: bad parameter declaration\n", line);
            exit(-1);
        }
        if (current_id[Class] == Loc) {
            printf("%d: duplicate parameter declaration\n", line);
            exit(-1);
        }

        match(Id);

        
//②
        
// store the local variable
        current_id[BClass] = current_id[Class]; current_id[Class]  = Loc;
        current_id[BType]  = current_id[Type];  current_id[Type]   = type;
        current_id[BValue] = current_id[Value]; current_id[Value]  = params++;   
// index of current parameter

        if (token == ',') {
            match(',');
        }
    }

   
// ③
    index_of_bp = params+1;
}
其中①与全局变量定义的解析十分一样,用于解析该参数的类型。

而②则与上节中提到的“局部变量覆盖全局变量”相关,先将全局变量的信息保存(无论是是否真的在全局中用到了这个变量)在 BXXX 中,再赋上局部变量相关的信息,如 Value 中存放的是参数的位置(是第几个参数)。

③则与汇编代码的生成有关,index_of_bp 就是前文提到的 new_bp 的位置。

函数体的解析

我们实现的 C 语言与现代的 C 语言不太一致,我们需要所有的变量定义出现在所有的语句之前。函数体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
void function_body() {
   
// type func_name (...) {...}
   
//                   -->|   |<--

   
// ... {
   
// 1. local declarations
   
// 2. statements
   
// }

    int pos_local;
// position of local variables on the stack.
    int type;
    pos_local = index_of_bp;

   
// ①
    while (token == Int || token == Char) {
        
// local variable declaration, just like global ones.
        basetype = (token == Int) ? INT : CHAR;
        match(token);

        while (token != ';') {
            type = basetype;
            while (token == Mul) {
                match(Mul);
                type = type + PTR;
            }

            if (token != Id) {
               
// invalid declaration
                printf("%d: bad local declaration\n", line);
                exit(-1);
            }
            if (current_id[Class]) {
               
// identifier exists
                printf("%d: duplicate local declaration\n", line);
                exit(-1);
            }
            match(Id);

            
// store the local variable
            current_id[BClass] = current_id[Class]; current_id[Class]  = Loc;
            current_id[BType]  = current_id[Type];  current_id[Type]   = type;
            current_id[BValue] = current_id[Value]; current_id[Value]  = ++pos_local;   
// index of current parameter

            if (token == ',') {
                match(',');
            }
        }
        match(';');
    }

   
// ②
   
// save the stack size for local variables
    *++text = ENT;
    *++text = pos_local - index_of_bp;

   
// statements
    while (token != '}') {
        statement();
    }

   
// emit code for leaving the sub function
    *++text = LEV;
}
其中①用于解析函数体内的局部变量的定义,代码与全局的变量定义几乎一样。

而②则用于生成汇编代码,我们在第三章的虚拟机中提到过,我们需要在栈上为局部变量预留空间,这两行代码起的就是这个作用。

代码

本章的代码可以在 Github 上下载,也可以直接 clone

1
git clone -b step-4 https:
//github.com/lotabout/write-a-C-interpreter
本章的代码依旧无法运行,还有两个重要函数没有完成:statement 及 expression,感兴趣的话可以尝试自己实现它们。

小结

本章中我们用了不多的代码完成了函数定义的解析。大部分的代码依旧是用于解析变量:参数和局部变量,而它们的逻辑和全局变量的解析几乎一致,最大的区别就是保存的信息不同。

当然,要理解函数定义的解析过程,最重要的是理解我们会为函数生成怎样的汇编代码,因为这决定了我们需要从解析中获取什么样的信息(例如参数的位置,个数等),而这些可能需要你重新回顾一下“虚拟机”这一章,或是重新学习学习汇编相关的知识。

下一章中我们将讲解最复杂的表达式的解析,同时也是整个编译器最后的部分,敬请期待。
手把手教你做一个 C 语言编译器(7):语句
整个编译器还剩下最后两个部分:语句和表达式的解析。它们的内容比较多,主要涉及如何将语句和表达式编译成汇编代码。这章讲解语句的解析,相对于表达式来说它还是较为容易的。

本系列:

手把手教你做一个 C 语言编译器(0):前言
手把手教你做一个 C 语言编译器(1):设计
手把手教你做一个 C 语言编译器(2):虚拟机
手把手教你做一个 C 语言编译器(3):词法分析器
手把手教你做一个 C 语言编译器(4):递归下降
手把手教你做一个 C 语言编译器(5):变量定义
手把手教你做一个 C 语言编译器(6):函数定义
语句

C 语言区分“语句”(statement)和“表达式”(expression)两个概念。简单地说,可以认为语句就是表达式加上末尾的分号。

在我们的编译器中共识别 6 种语句:

if (...) <statement> [else <statement>]
while (...) <statement>
{ <statement> }
return xxx;
<empty statement>;
expression; (expression end with semicolon)
它们的语法分析都相对容易,重要的是去理解如何将这些语句编译成汇编代码,下面我们逐一解释。

IF 语句

IF 语句的作用是跳转,跟据条件表达式决定跳转的位置。我们看看下面的伪代码:

1
2
3
4
5
6
7
8
9
if (...) <statement> [else <statement>]

  if (<cond>)                   <cond>
                                JZ a
    <true_statement>   ===>     <true_statement>
  else:                         JMP b
a:                           a:
    <false_statement>           <false_statement>
b:                           b:
对应的汇编代码流程为:

执行条件表达式 <cond>。
如果条件失败,则跳转到 a 的位置,执行 else 语句。这里 else 语句是可以省略的,此时 a 和 b 都指向 IF 语句后方的代码。
因为汇编代码是顺序排列的,所以如果执行了 true_statement,为了防止因为顺序排列而执行了 false_statement,所以需要无条件跳转 JMP b。
对应的 C 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (token == If) {
    match(If);
    match('(');
    expression(Assign);  
// parse condition
    match(')');

    *++text = JZ;
    b = ++text;

    statement();         
// parse statement
    if (token == Else) {
// parse else
        match(Else);

        
// emit code for JMP B
        *b = (int)(text + 3);
        *++text = JMP;
        b = ++text;

        statement();
    }

    *b = (int)(text + 1);
}
While 语句

While 语句比 If 语句简单,它对应的汇编代码如下:

1
2
3
4
5
6
a:                     a:
   while (<cond>)        <cond>
                         JZ b
    <statement>          <statement>
                         JMP a
b:                     b:
没有什么值得说明的内容,它的 C 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
else if (token == While) {
    match(While);

    a = text + 1;

    match('(');
    expression(Assign);
    match(')');

    *++text = JZ;
    b = ++text;

    statement();

    *++text = JMP;
    *++text = (int)a;
    *b = (int)(text + 1);
}
Return 语句

Return 唯一特殊的地方是:一旦遇到了 Return 语句,则意味着函数要退出了,所以需要生成汇编代码 LEV 来表示退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
else if (token == Return) {
   
// return [expression];
    match(Return);

    if (token != ';') {
        expression(Assign);
    }

    match(';');

   
// emit code for return
    *++text = LEV;
}
其它语句

其它语句并不直接生成汇编代码,所以不多做说明,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else if (token == '{') {
   
// { <statement> ... }
    match('{');

    while (token != '}') {
        statement();
    }

    match('}');
}
else if (token == ';') {
   
// empty statement
    match(';');
}
else {
   
// a = b; or function_call();
    expression(Assign);
    match(';');
}
代码

本章的代码可以在 Github 上下载,也可以直接 clone

1
git clone -b step-5 https:
//github.com/lotabout/write-a-C-interpreter
本章的代码依旧无法运行,还剩最后一部分没有完成:expression。

小结

本章讲解了如何将语句编译成汇编代码,内容相对容易一些,关键就是去理解汇编代码的执行原理。

同时值得一提的是,编译器的语法分析部分其实是很简单的,而真正的难点是如何在语法分析时收集足够多的信息,最终把源代码转换成目标代码(汇编)。我认为这也是初学者实现编译器的一大难点,往往比词法分析/语法分析更困难。

所以建议如果没有学过汇编,可以学习学习,它本身不难,但对理解计算机的原理有很大帮助。
手把手教你做一个 C 语言编译器(8):表达式
整个编译器还剩下最后两个部分:语句和表达式的解析。它们的内容比较多,主要涉及如何将语句和表达式编译成汇编代码。这章讲解语句的解析,相对于表达式来说它还是较为容易的。

本系列:

手把手教你做一个 C 语言编译器(0):前言
手把手教你做一个 C 语言编译器(1):设计
手把手教你做一个 C 语言编译器(2):虚拟机
手把手教你做一个 C 语言编译器(3):词法分析器
手把手教你做一个 C 语言编译器(4):递归下降
手把手教你做一个 C 语言编译器(5):变量定义
手把手教你做一个 C 语言编译器(6):函数定义
手把手教你做一个 C 语言编译器(7):语句
语句

C 语言区分“语句”(statement)和“表达式”(expression)两个概念。简单地说,可以认为语句就是表达式加上末尾的分号。

在我们的编译器中共识别 6 种语句:

if (...) <statement> [else <statement>]
while (...) <statement>
{ <statement> }
return xxx;
<empty statement>;
expression; (expression end with semicolon)
它们的语法分析都相对容易,重要的是去理解如何将这些语句编译成汇编代码,下面我们逐一解释。

IF 语句

IF 语句的作用是跳转,跟据条件表达式决定跳转的位置。我们看看下面的伪代码:

1
2
3
4
5
6
7
8
9
if (...) <statement> [else <statement>]

  if (<cond>)                   <cond>
                                JZ a
    <true_statement>   ===>     <true_statement>
  else:                         JMP b
a:                           a:
    <false_statement>           <false_statement>
b:                           b:
对应的汇编代码流程为:

执行条件表达式 <cond>。
如果条件失败,则跳转到 a 的位置,执行 else 语句。这里 else 语句是可以省略的,此时 a 和 b 都指向 IF 语句后方的代码。
因为汇编代码是顺序排列的,所以如果执行了 true_statement,为了防止因为顺序排列而执行了 false_statement,所以需要无条件跳转 JMP b。
对应的 C 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (token == If) {
    match(If);
    match('(');
    expression(Assign);  
// parse condition
    match(')');

    *++text = JZ;
    b = ++text;

    statement();         
// parse statement
    if (token == Else) {
// parse else
        match(Else);

        
// emit code for JMP B
        *b = (int)(text + 3);
        *++text = JMP;
        b = ++text;

        statement();
    }

    *b = (int)(text + 1);
}
While 语句

While 语句比 If 语句简单,它对应的汇编代码如下:

1
2
3
4
5
6
a:                     a:
   while (<cond>)        <cond>
                         JZ b
    <statement>          <statement>
                         JMP a
b:                     b:
没有什么值得说明的内容,它的 C 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
else if (token == While) {
    match(While);

    a = text + 1;

    match('(');
    expression(Assign);
    match(')');

    *++text = JZ;
    b = ++text;

    statement();

    *++text = JMP;
    *++text = (int)a;
    *b = (int)(text + 1);
}
Return 语句

Return 唯一特殊的地方是:一旦遇到了 Return 语句,则意味着函数要退出了,所以需要生成汇编代码 LEV 来表示退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
else if (token == Return) {
   
// return [expression];
    match(Return);

    if (token != ';') {
        expression(Assign);
    }

    match(';');

   
// emit code for return
    *++text = LEV;
}
其它语句

其它语句并不直接生成汇编代码,所以不多做说明,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else if (token == '{') {
   
// { <statement> ... }
    match('{');

    while (token != '}') {
        statement();
    }

    match('}');
}
else if (token == ';') {
   
// empty statement
    match(';');
}
else {
   
// a = b; or function_call();
    expression(Assign);
    match(';');
}
代码

本章的代码可以在 Github 上下载,也可以直接 clone

1
git clone -b step-5 https:
//github.com/lotabout/write-a-C-interpreter
本章的代码依旧无法运行,还剩最后一部分没有完成:expression。

小结

本章讲解了如何将语句编译成汇编代码,内容相对容易一些,关键就是去理解汇编代码的执行原理。

同时值得一提的是,编译器的语法分析部分其实是很简单的,而真正的难点是如何在语法分析时收集足够多的信息,最终把源代码转换成目标代码(汇编)。我认为这也是初学者实现编译器的一大难点,往往比词法分析/语法分析更困难。

所以建议如果没有学过汇编,可以学习学习,它本身不难,但对理解计算机的原理有很大帮助。
手把手教你做一个 C 语言编译器(9):总结
恭喜你完成了自己的 C 语言编译器,本章中我们发一发牢骚,说一说编写编译器值得注意的一些问题;编写编译器时遇到的一些难题。

本系列:

手把手教你做一个 C 语言编译器(0):前言
手把手教你做一个 C 语言编译器(1):设计
手把手教你做一个 C 语言编译器(2):虚拟机
手把手教你做一个 C 语言编译器(3):词法分析器
手把手教你做一个 C 语言编译器(4):递归下降
手把手教你做一个 C 语言编译器(5):变量定义
手把手教你做一个 C 语言编译器(6):函数定义
手把手教你做一个 C 语言编译器(7):语句
手把手教你做一个 C 语言编译器(8):表达式
虚拟机与目标代码

整个系列的一开始,我们就着手虚拟机的实现。不知道你是否有同感,这部分对于整个编译器的编写其实是十分重要的。我认为至少占了重要程度的50%。

这里要说明这样一个观点,学习编译原理时常常着眼于词法分析和语法分析,而忽略了同样重要的代码生成。对于学习或考试而言或许可以,但实际编译项目时,最为重要的是能“跑起来”,所以我们需要给予代码生成高度的重视。

同时我们也看到,在后期解析语句和表达式时,难点已经不再是语法分析了,而是如何为运算符生成相应的汇编代码。

词法分析

我们用了很暴力的手段编写了我们的词法分析器,我认为这并无不可。

但你依旧可以学习相关的知识,了解自动生成词法分析器的原理,它涉及到了“正则表达式”,“状态机”等等知识。相信这部分的知识能够很大程度上提高你的编程水平。

同时,如果今后你仍然想编写编译器,不妨试试这些自动生成工具。

语法分析

长期以来,语法分析对我而言一直是迷一样的存在,直到真正用递归下降的方式实现了一个。

我们用了专门的一章讲解了“递归下降”与 BNF 文法的关系。希望能减少你对理论的厌恶。至少,实现起来并不是太难。

如果有兴趣,可以学习学习这些文法,因为已经有许多自动生成的工具支持它们。这样你就不需要重复造轮子。可以看看 yacc 等工具,更先进的版本是 bsion。同时其它语言也有许多类似的支持。

题外话,最近知道了一个叫“PEG 文法”的表示方法,无论是读起来,还是实现起来,都比 BNF 要容易,你也可以学习看看。

关于编代码

这也是我自己的感慨吧。无论多好的教程,想要完全理解它,最好的方式恐怕还是要自己实现它。

只是在编写代码的过程中,我们会遇到许多的挫折,例如需要考虑许多细节,或是调试起来十分困难。但也只有真正静下心来去克服它,我们才能有所成长吧。

例如在编写表达式的解析时,大量重复的代码特别让人崩溃。还有就是调试编译器,简直痛苦地无话可说。

P.S. 如果你按这个系列自己编写代码,记得事先写一些用于输出汇编代码的函数,很有帮助的。

还有就是写这个系列的文章,开始的冲动过了之后,每写一篇都特别心烦,希望文章本身没有受我的这种情绪影响吧。

结语

编程有趣又无趣,只有身在其中的我们才能体会吧。

2016-2-2 23:17
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 5 楼』:  4学习较底层编程:动手写一个C语言编译器

学习较底层编程:动手写一个C语言编译器

2013/07/19 · C/C++, 开发 · 1 评论 · C语言, 编译器
分享到:
本文由 伯乐在线 - 菜鸟浮出水 翻译。未经许可,禁止转载!
英文出处:Wilfred Hughes。欢迎加入翻译组。
动手编写一个编译器,学习一下较为底层的编程方式,是一种学习计算机到底是如何工作的非常有效方法。

编译器通常被看作是十分复杂的工程。事实上,编写一个产品级的编译器也确实是一个庞大的任务。但是写一个小巧可用的编译器却不是这么困难。

秘诀就是首先去找到一个最小的可用工程,然后把你想要的特性添加进去。这个方法也是Abdulaziz Ghuloum在他那篇著名的论文“一种构造编译器的捷径”里所提到的办法。不过这个办法确实可行。你只需要按照这篇论文中的第一步来操作,就可以得到一个真正可用的编译器!当然,它只能编译程序语言中的非常小的子集,但是它确实是一个真实可用的编译器。你可以随意的扩展这个编译器,然后从中学到更多更深的知识。

受到这篇文章的鼓舞,我就写了一个C编译器。从某种意义上来说这比写一个scheme的编译器要困难一些(因为你必须去解析C那复杂的语法),但是在某些方面又很便利(你不需要去处理运行时类型)。要写这样一个编译器,你只需要从你那个可用的最小的编译器开始。

对于我写的编译器来说,我把它叫 babyc,我选了这段代码来作为我需要运行的第一个程序:

int main() {
    return 2;
}
没有变量,没有函数调用,没有额外的依赖,甚至连if语句,循环语句都没有,一切看起来是那么简单。

我们首先需要解析这段代码。我们将使用 Flex 和 Bison 来做到这点。这里有怎么用的例子可以参考,幸好我们的语法是如此简单,下面就是词法分析器:

"{" { return '{'; }
"}" { return '}'; }
"(" { return '('; }
")" { return ')'; }
";" { return ';'; }
[0-9]+ { return NUMBER; }
"return" { return RETURN; }
"int" { return TYPE; }
"main" { return IDENTIFIER; }
这里是语法分析器:

function:
        TYPE IDENTIFIER '(' ')' '{' expression '}'
        ;

expression:
        RETURN NUMBER ';'
        ;
最终,我们需要生成一些汇编代码。我们将使用32位的X86汇编,因为它非常的通用而且可以很容易的运行在你的机器上。这里有X86汇编的相关网站。

下面就是我们需要生成的汇编代码:

.text
        .global _start # Tell the loader we want to start at _start.

_start:
        movl    $2,%ebx # The argument to our system call.
        movl    $1,%eax # The system call number of sys_exit is 1.
        int     $0x80 # Send an interrupt
然后加上上面的词法语法分析代码,把这段汇编代码写进一个文件里。恭喜你!你已经是一个编译器的编写者了!

Babyc 就是这样诞生的,你可以在这里看到它最开始的样子。

当然,如果汇编代码没办法运行也是枉然。让我们来用编译器生成我们所希望的真正的汇编代码。

# Here's the file we want to compile.
$ cat return_two.c
#include <stdio.h>

int main() {
    return 2;
}

# Run the compiler with this file.
$ ./babyc return_two.c
Written out.s.

# Check the output looks sensible.
$ cat out.s
.text
    .global _start

_start:
    movl    $2, %ebx
    movl    $1, %eax
    int     $0x80
非常棒!接着让我们来真正的运行一下编译之后代码来确保它能得到我们所想的结果。

# Assemble the file. We explicitly assemble as 32-bit
# to avoid confusion on x86_64 machines.
$ as out.s -o out.o --32

# Link the file, again specifying 32-bit.
$ ld -m elf_i386 -s -o out out.o

# Run it!
$ ./out

# What was the return code?
$ echo $?
2 # Woohoo!
我们踏出了第一步,接下去怎么做就全看你了。你可以按照那篇文章所指导的全部做一遍,然后制作一个更加复杂的编译器。你需要去写一个更加精巧的语法树来生成汇编代码。接下去的几步分别是:(1)允许返回任意的值(比如,return 3; 一些可执行代码);(2)添加对“非”的支持(比如,return ~1; 一些可执行代码)。每一个额外的特性都可以教你关于C语言的更多知识,编译器到底是怎么执行的,以及世界上其他编写编译器的人是如何想的。

这是构建 babyc 的方法。Babyc 现在已经拥有了if语句,循环,变量以及最基础的数据结构。欢迎你来check out它的代码,但是我希望看完我的文章你能够自己动手写一个。

不要害怕底层的一些事情。这是一个非常奇妙的世界。

2016-2-2 23:33
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 6 楼』:  5自制编译器:语法分析器 6楼

自制编译器:语法分析器(一)
2013-04-29 22:45 3502人阅读 评论(0) 收藏 举报
分类:  自制Compiler(7)  
版权声明:本文为博主原创文章,未经博主允许不得转载。

感觉语法分析器在编译器前端是一个较为庞大的东西,因此打算分两篇网志来描述,第一篇着重描述思想,第二篇具体论述实现。

1、语法分析器要做什么
在编写任何一个东西的的时候,都要先弄明白这个玩意儿是做什么的,接受什么输入,产生什么输出。
一个语法分析器要接受词法分析器所产生的词素作为输入,产生一个抽象语法树给中间代码生成器,然后再由中间代码生成器生成中间代码并递交给编译器后端。当然在某些理解中可以把抽象语法树就当做是一种中间代码的表示形式,直接递交给后端。不管怎么说,总之就是语法分析器是一个生成抽象语法树的东西。
值得注意的是,语法分析器不仅要生成抽象语法树,而且还要在这个生成过程中找出各种语法错误并生成和维护符号表。

2、符号表
什么是符号表?符号表有什么用?
所谓符号表就是一个记录各种标识符(也就是终结符号id,词素id)及其属性的表,比如记录一个int变量x的类型为int,它的作用域,记录一个函数名,记录其函数类型,参数列表等。
符号表有作用域,比如一段简单的代码:
[java] view plain copy
public void function()  
{  
   int i=0;  
   while(true)  
   {  
      int i=1;  
   }  
}  
因此一个符号表的构造一定是一个树状结构,我们在编译器中用以下结构来描述一个符号表:
[java] view plain copy
package ravaComplier.symTable;  
import java.util.*;  
public class SymTable {  
    private SymTable fatherSymTable;  
    private ArrayList<SymTable> blockSymTables;  
    private HashMap<String,Symbol> table;  
    private String blockid;  
    public SymTable(SymTable st,String str)  
    {  
        fatherSymTable=st;  
        blockid=str;  
        blockSymTables=new ArrayList<SymTable>();  
        table=new HashMap<String,Symbol>();  
    }  
    public void addSym(Symbol sym)  
    {  
        table.put(sym.id, sym);  
    }  
    public Symbol getSym(String id)  
    {  
        Symbol result=table.get(id);  
        if(result==null && fatherSymTable!=null)  
        {  
            return fatherSymTable.getSym(id);  
        }  
        return result;  
    }  
    public void addSymTable(SymTable st)  
    {  
        blockSymTables.add(st);  
    }  
}  
代码很简单以至于注释都懒得写了。
通过fatherSymTable来记录此符号表的父表,用于不断的向上回溯查找符号(getSym)使用。
blockid算是给此表一个id,用于打印调试信息时使用。
addSym在此表增加符号。除此之外还有个addSymTables来加入子表。
另外此类还重载了toString()方法,用于debug信息,限于篇幅这个方法没贴到网志里,可在我上传的资源里拿到完整的源文件。
也许在之后的分析描述写代码的过程中我会发现需要给这个类添加新的函数,到那时再对此类进行补充。
接下来看看简单的Symbol类,也就是表示一个符号的类:
[java] view plain copy
package ravaComplier.symTable;  
  
public class Symbol {  
    public String id;  
    public int type;  
    public Object value;  
    public Symbol(String i,int t,Object v)  
    {  
        id=i;  
        type=t;  
        value=v;  
    }  
    public static int TYPE_CLASSNAME=0;  
    public static int TYPE_MEMBERVAR=1;  
    public static int TYPE_LOCALVAR=2;  
    public static int TYPE_FUNCNAME=3;  
    public static int TYPE_CONSFUNC=4;  
}  
分为3个域,id,也就是标识符,type,枚举值已列出,value,根据不用的枚举值定义了不同的value,之后若用到了再贴代码吧。
总共分为5类符号:类名、成员变量、局部变量、函数名和构造函数名。
当然若之后根据需要,或许会使用新的符号也可以灵活的添加。

3、语法树的表示
一棵语法树不能使用普通的树结构因为每个不同的节点的行为、值太多且不同。语法树中的节点为非终结符号或者终结符号,对于其中的id,我们就让它指向符号表中的符号即可,对于非终结符号,每个非终结符号我们都建立一个新的类来描述其行为,对于非id的终结符号,其信息要么不记录(比如无意义的分好括号等),要么简单记录其类型(比如各种运算符)。
所以这种情况下每一个节点的建立都比较灵活,下面举两个例子:
对于产生式:ops --> bitop | logiop | artmop | cprop
我们建立如下的类来描述ops:
[java] view plain copy
package ravaComplier.syntax.nodes;  
  
public class ops {  
    private int type;  
    private Object value; //must be bitop,logiop,artmop,cprop  
    public ops()  
    {  
        //not implements  
    }  
    public static int TYPE_BITOP=0;  
    public static int TYPE_LOGIOP=1;  
    public static int TYPE_ARTMOP=2;  
    public static int TYPE_CPROP=3;  
}  
int描述运算符类型,然后根据响应的类型让value为具体的运算符类。接着给出cprop的类:
[java] view plain copy
package ravaComplier.syntax.nodes;  
  
public class cprop {  
    public cprop()  
    {  
        //not implemets  
    }  
    private int type;  
    public static int  TYPE_GREATER = 0;//>  
    public static int TYPE_GREATEREQUAL=1;//>=;  
    public static int TYPE_LESS=2;//<;  
    public static int TYPE_LESSEQUEAL=3;//<=;  
    public static int TYPE_EQUAL=4;//==  
}  
这是一个终结符,所以只有一个域来记录运算符的类型。
本篇文章到此就结束了,下篇文章讲着重分析语法树的展开过程。

自制编译器:语法分析器(二)
2013-05-16 23:13 5183人阅读 评论(1) 收藏 举报
分类:  自制Compiler(7)  
版权声明:本文为博主原创文章,未经博主允许不得转载。
这篇博文拖了好久才写完,其一是语法分析器本身的难度实在有点超出我的预料,以至于反复重构多次才完成,其二是因为弄了个xbox玩,占用了一部分的课余时间= =!。
本篇博文将分为以下几个部分来描述一下语法分析器的具体实现,理论、部分典型节点与结果。

一、语法制导翻译、LL与LL(X)语法、左递归等其它
为什么要写一个语法分析器?语法分析器的作用不仅仅是来检查词素列表中的序列是否是一个我们语言的语句,更重要的是只有借助语法分析器得到的抽象语法树,才能够生成中间代码或者具体的目标代码,这个过程叫做语法制导翻译(syntax-directed translation)。在紫龙书(编译原理第二版)的封面上,一个拿盾的骑士正在和一个喷火龙决斗,其中龙的身上写的是Complexity of Complier Design,而骑士的盾上写的则是Syntax Directed Translation,因此把语法制导翻译当作是编译器前端的核心也不为过。

展开语法树的过程实质上也就是将词素不断地对应到我们语言定义的递推式的过程,换个说法其实也就是不断地展开语言的递推式,使之符合已有词素的过程。这个展开的过程从方法上来讲可分为两种:LL和LR。其中第一个字母代表从左到右读词素序列,第二个字母L代表尝试最先展开最左边的非终结符号,R代表尝试从右边开始将词素归约为非终结符好。换言之,LL是一种自顶向下的展开方法,LR是一种自底向上的归约方法,本文采用的技术为LL,所以以下也以讨论LL为主。

为了使编译器能高效迅速,一个良好的语法设计必须是一个LL(1)语法,什么是LL(1)语法呢?举个例子,当我们面对如下推导式的时候:
ids-> id|               ----------1
          id.ids|        -----------2
         ids[expr] |   -----------3
         this
此时我们读到了一个词素id,是展开成1、2、3中的哪种呢?当然目前我们无法判断,因此需要多读入下一个词素才能进行判断。如果读到的是[,则展开成3。如果读到了.则展开成2,否则展开成1。但问题是有些情况下,多读入一个词素或许还不能进行判断,当一个语言的语法中,只要多读入X个词素就能唯一的确定推导式,则称其为LL(X)文法。很遗憾,我们的语法不是LL(1)语法,虽然有很多推导式的处理技巧可以将一个非LL(1)的语法处理成LL(1)的语法,但这样会失去语法的直观性。考虑再三我在“不是很合理但易于理解的语法” 和 “合理高效的不直观的语法” 之间选择了前者。
因此既然我们的语法并非LL(1)的,因此在语法分析的过程中,我们只是不断的去尝试展开,如果不成功,则回溯。虽然这是比较低效的,但文法中的大多数推导式并不复杂,所以处理的时间完全可以接受。
考虑如下 推导式:
expr -->  (expr)     ------------1
                ids|       ------------2
              number|    -----------3
              literal|      ------------4
             func-call|   ------------5
            expr  ops expr|
这个推导式不满足LL(1),假设当前读到了一个id,目前可供选择的有2、4、5,然后又读入了一个“。”,目前可供选择的还是2、4、5,又读入了一个id,可供选择的还是2、4、5,然后读入了一个“(” ,这时候才能确定唯一的展开式func-call。但这个表达式除了不满足LL(1)之外还有其它的问题:左递归。
假设expr上来就尝试去展开成5的形式,因为是递归展开的过程,5中最左边的expr又会尝试展开成5的形式,然后这个过程就不断递归下去最终导致stack overflow。虽然有很多方法和技巧可以改变推导式的形式来消除左递归,但是依然本着易于理解的原则,我们在语法分析中通过使用朴素的笨办法来避免这种情况的发生。所谓的笨办法就是:
(1)按优先级先展开1234,然后都失败再展开成5。
(2)设置最大展开深度为200,超过了直接报错。
虽然很笨很低效,但勉强够用了。

二、语法分析器结构
语法分析器在实现上分以下几个部分,第一部分为SyntaxTreeGenerator,负责读入词素,和词法分析器以及后端程序进行交流,算是语法分析器的对外接口。其次使用GlobalVars来存储各种全局数据,记录分析过程中的各种信息。最后就是各种节点,每个节点在分析的过程中若需要其它信息则通过GlobalVars来解耦。接下来通过几个例子来具体说明这些节点是如何展开语法分析的:

(1)id
[java] view plain copy
package ravaComplier.syntax.nodes;  
  
import ravaComplier.lexer.Lexeme;  
import ravaComplier.symTable.SymTable;  
import ravaComplier.symTable.Symbol;  
import ravaComplier.syntax.GlobalVars;  
import ravaComplier.syntax.SyntaxTreeGenerator;  
  
/*
* 该类尝试读入词素并生成id节点
*/  
public class id {  
    public SymTable curST;//这个节点的符号表  
    public id() throws Exception  
    {  
      
        Lexeme lex=SyntaxTreeGenerator.readLexeme();//读一个词素  
        curST=SyntaxTreeGenerator.getCurTable();//得到当前符号表  
        if(lex.type==Lexeme.ID)  
        {  
            //类型正确  
            symEntry=SyntaxTreeGenerator.getCurTable().getSym(lex.value.toString());//判断符号表中是否已有此id  
            if(symEntry==null)  
            {  
                firstappear=true;  
                  
            }  
            else  
            {  
                firstappear=false;  
            }  
            value=lex.value.toString();  
              
            symEntry=new Symbol(value,2,null);//生成一个入口  
        }  
        else  
        {  
            //类型错误抛出异常  
            throw new Exception("ID required!\r\n");  
              
        }  
        GlobalVars.idlist.add(this);//把所有id都添加进idlist里。  
    }  
    public String value;  
    public boolean firstappear;  
    public type tp;//类型,由调用者赋值  
    public Symbol symEntry;//指向符号表的条目  
}  
这个类代表id节点,首先尝试读入词素,如果不是id则发生语法错误。其次需要判断此id是否是第一次出现,在某些时候这个信息很重要(比如变量定义时),然后最后将已经初始化好的id添加到GlobalVars的list中。值得注意的是,GlobalVars里面有很多的list,主要是用于在生成语法树之后用于一些检查。
(2)vardeclare
来个稍微复杂点的,局部变量的定义
[java] view plain copy
package ravaComplier.syntax.nodes;  
  
import java.util.ArrayList;  
  
import ravaComplier.lexer.Lexeme;  
import ravaComplier.symTable.SymTable;  
import ravaComplier.symTable.Symbol;  
import ravaComplier.syntax.SyntaxTreeGenerator;  
  
public class vardeclare {  
    /*
     * var-declare --> type args|type[] args
     *         
     */  
    public SymTable curST;  
    public vardeclare() throws Exception  
    {  
        curST=SyntaxTreeGenerator.getCurTable();  
        tp=new type();  
        int pos=SyntaxTreeGenerator.savePos();//得到当前分析的位置  
        Lexeme lex=SyntaxTreeGenerator.readLexeme();//读取下一个词素  
        arrayDeclare=false;  
        if(!lex.value.equals("["))  
        {  
            SyntaxTreeGenerator.loadPos(pos);//若不是想要的词素则回溯  
        }  
        else  
        {  
            lex=SyntaxTreeGenerator.readLexeme();  
            if(!lex.value.equals("]"))  
            {  
                SyntaxTreeGenerator.loadPos(pos);  
                throw new Exception("] expected!");//发生语法错误,数组定义时括号没有闭合。  
            }  
            else  
            {  
                arrayDeclare=true;  
                  
            }  
        }  
        ags=new args();  
        ArrayList<ids> al=ags.getidsList();//获取参数列表,若args为   a1,a2,a3则返回的列表中含有a1,a2,a3  
        SymTable st=SyntaxTreeGenerator.getCurTable();  
        for(int i=0;i<=al.size()-1;i++)  
        {  
            id ID=al.get(i).getLastID();//id1.id2.id3则此函数返回id3。  
            if(ID.firstappear==false)  
            {  
                throw new Exception("id declared duplicated!");//定义的变量名已经出现过了,报错  
            }  
            st.addSym(ID.symEntry);//将id添加进符号表  
            ID.symEntry.value=ID;  
            ID.symEntry.type=Symbol.TYPE_LOCALVAR;  
            ID.tp=tp;//给此id赋予类型  
            if(arrayDeclare)  
            {  
                ID.tp.isArray=true;  
            }  
        }  
    }  
    public type tp;  
    public args ags;  
    public boolean arrayDeclare;  
}  
可以看出,id节点中的很多属性都是由其调用者决定的,这点在节点逻辑的编写中体现的尤为明显。

(3)memberfundeclare
来个再复杂点的,成员函数定义:
[java] view plain copy
package ravaComplier.syntax.nodes;  
  
import ravaComplier.lexer.Lexeme;  
import ravaComplier.symTable.SymTable;  
import ravaComplier.symTable.Symbol;  
import ravaComplier.syntax.SyntaxTreeGenerator;  
  
public class memberfuncdeclare {  
    public SymTable curST;  
    public memberfuncdeclare() throws Exception  
    {  
        /*member-func-declare --> private|public
                                  NUL|static
                                  type func-name(  NUL|def-args )  {  func-body  }*/  
        curST=SyntaxTreeGenerator.getCurTable();  
        af=new accessflag();//得到一个accessflag, 即public 或者 private  
        //尝试读取static ,若没有则回溯。  
        int pos=SyntaxTreeGenerator.savePos();  
        Lexeme lex=SyntaxTreeGenerator.readLexeme();  
        if(lex.type!=Lexeme.STATIC)  
        {  
            SyntaxTreeGenerator.loadPos(pos);  
        }  
        else  
        {  
              
            isstatic=true;  
        }  
        tp=new type();//得到type  
        fc=new funcname();//得到函数名。  
        fc.id.symEntry.value=this;  
        if(fc.id.firstappear==false)  
        {  
            //判断函数名是否重复,若重复则报错。  
            throw new Exception("function name must be unique!");  
        }  
        SymTable st=SyntaxTreeGenerator.getCurTable();  
        st.addSym(fc.id.symEntry);//把这个函数添加进符号表  
        fc.id.symEntry.type=Symbol.TYPE_FUNCNAME;  
        SymTable st1=new SymTable(st,fc.id.value+" symtable");//建立一个子表,每个函数都有自己的符号表因为里面变量的作用域和其外不同  
        SyntaxTreeGenerator.setCurTable(st1);//将子表设置为当前符号表,之后该函数体内的一切分析都使用该表  
        lex=SyntaxTreeGenerator.readLexeme();  
        if(!lex.value.toString().equals("("))  
        {  
            throw new Exception("( expected!");//语法检查  
        }  
        try  
        {  
            pos=SyntaxTreeGenerator.savePos();  
        da=new defargs();//尝试搜寻其后的调用参数,若没有参数则根据上一行存储的位置回滚  
         
        }  
        catch(Exception e)  
        {  
            SyntaxTreeGenerator.loadPos(pos);  
            da=null;  
        }  
        lex=SyntaxTreeGenerator.readLexeme();  
        if(!lex.value.toString().equals(")"))  
        {  
            throw new Exception(") expected!");//语法检查  
        }  
        lex=SyntaxTreeGenerator.readLexeme();  
        if(!lex.value.toString().equals("{"))  
        {  
            throw new Exception("{ expected!");//语法检查  
        }  
        fb=new funcbody();//构造函数体  
        lex=SyntaxTreeGenerator.readLexeme();  
        if(!lex.value.toString().equals("}"))  
        {  
            throw new Exception("} expected!");//语法检查  
        }  
        SyntaxTreeGenerator.setCurTable(st);//函数结束,重置符号表  
    }  
    public accessflag af;  
    public type tp;  
    public boolean isstatic;  
    public funcname fc;  
    public defargs da;  
    public funcbody fb;  
}  


通过以上的分析,我们可以总结出每一个节点的构造规则:
1、尝试将此节点按一定的顺序展开
2、其每一个部分当作该节点的成员变量
3、在展开的时候和符号表进行适当的交互

按照类似的思路,我们当我们完成所有节点后,编译器的前端也已经差不多了,下图是上篇博文中日志里的示例程序得到的语法树,可以看到即便是一个简单的示例程序,其语法树也相当复杂。

[ Last edited by zzz19760225 on 2016-2-3 at 01:15 ]

2016-2-3 01:07
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 7 楼』:  6自制编译器:词法分析器 7楼

自制编译器:词法分析器
2013-04-21 23:03 1573人阅读 评论(1) 收藏 举报
分类:  自制Compiler(7)  
版权声明:本文为博主原创文章,未经博主允许不得转载。

词法分析器代码已上传到个人资源中。
当我们的程序源文件进入编译器,首先遇到的就是词法分析器。
词法分析器的作用就是解析源文件,分析出其中的词素,并把这个词素的顺序集输入给语法分析器。
接上篇把所谓的词素也就是终结符号列出来:
if else while ( ) { } cpreop bitop logiop armtcop number literal id NUL new [ ] basetype class private public static return break continue . this
其中cprop包括 > < >= <= == != 即比较运算符
bitop 为位运算符,包括<< >> & | ^
logiop 逻辑运算符 包括 && ||
armtcop 算数运算符 包括 + - * /
number 数字常量 例如12345整形火 1.2345小数
id 标识符 按java规则
literal 字符串常量 如"ROgerwong"
NUL 空串
basetype 基本类型 包括 int char double 三种
当然,为了简单,在这里并不打算讨论非确定有穷自动机和确定有穷自动机的理论以及其之间的转换算法,只是用最朴素的方法,不断的将字符读入缓冲区,然后和这些词素进行比较,然后把这个词素加入到一个ArrayList中。
按着这个方法定义几个数据结构:
定义词素数据结构,共含两个域,1个表示类型,一个表示具体的值,类型的取值也已经标出。
[java] view plain copy
<p>package ravaComplier.lexer;</p><p>public class Lexeme {  
public int type;  
public Object value;  
public Lexeme(int t,Object v)  
{  
  type=t;  
  value=v;  
}  
@Override  
public String toString()  
{  
  return new String("<"+type+":"+value.toString()+">");  
}  
   
public static int IF=0;//if  
public static int ELSE=1;//else  
public static int WHILE=2;//while  
public static int BRACKET=3;//各种括号  
public static int CPREOP=4;//比较符号  
public static int BITOP=5;//位操作符  
public static int LOGIOP=6;//逻辑运算符  
public static int ARMTOP=7;//算术运算符  
public static int NUMBER=8;//立即数  
public static int LITERAL=9;//字符串  
public static int ID=10;//id  
public static int NUL=11;//空  
public static int NEW=12;//new 操作符  
public static int BASETYPE=13;//基本数据类型  
public static int CLASS=14;//关键字class  
public static int ACCESSFLAG=15;//public 或者private  
public static int STATIC=16;//关键字static  
public static int RETURN=17;//关键字return  
public static int BREAK=18;//break  
public static int CONTINUE=19;//continue  
public static int DOT=20;//.  
public static int THIS=21;//关键字this  
public static int SEMI=22;//分号  
public static int EQUAL=23;//等号  
}  
</p>  
其次,因为是用朴素的笨办法,所以我们需要构造规则:
定义分隔符:空格、制表符、换行符、+、-、*、/、.、;、各种括号运算符等。
若遇到分隔符,则分隔符前面的缓冲区为一个词素,分隔符为一个词素(空格、制表符、换行符)除外。
但注意特殊情况,若遇到>和>=,&和&& 之类的,需要多向前看一个字符来确定词素。
然后再把分割出的词素实例化成Lexeme类型,并加入到返回结果中。
代码很简单,但写起来比较费事:
[java] view plain copy
  
[java] view plain copy
package ravaComplier.lexer;  
  
import java.io.*;  
import java.util.*;  
  
public class Lexer {  
    private static ArrayList<Lexeme> result;//返回的结果  
    private static BufferedReader br;  
    private static StringBuffer buffer;//缓冲区  
  
    public static ArrayList<Lexeme> getLexerOutput(InputStream is)  
    {  
        result=new ArrayList<Lexeme>();  
        br=new BufferedReader(new InputStreamReader(is));  
        buffer=new StringBuffer();  
        while(Read())  
        {  
            addLexeme();  
        }  
        return result;  
    }  
    //尝试将缓冲区分解出词素并加入词素集合  
    private static void addLexeme()  
    {  
        String str=buffer.toString();  
        String endstr=str.substring(str.length()-1,str.length());  
        //判断单字符的分割符号  
        if(endstr.equals(" ") || endstr.equals("\t")  || endstr.equals(";") || endstr.equals("{") || endstr.equals("}") || endstr.equals("(") || endstr.equals(")") || endstr.equals("[") || endstr.equals("]") || endstr.equals("+") || endstr.equals("-") || endstr.equals("*") || endstr.equals("/") )  
        {  
            Lexeme lex=getLexeme(str.substring(0,str.length()-1));  
            if(lex!=null)  
            {  
                result.add(lex);  
            }  
            lex=getLexeme(endstr);  
            if(lex!=null)  
            {  
                result.add(lex);  
            }  
              
            buffer=new StringBuffer();  
        }  
        //判断双字符的分割符号  
        if(str.length()>=2)  
        {  
            endstr=str.substring(str.length()-2,str.length());  
            if(endstr.equals(">=") ||endstr.equals("<=") ||endstr.equals("==") || endstr.equals("||") ||endstr.equals("&&") || endstr.equals("!=") ||endstr.equals("\r\n"))  
            {  
                Lexeme lex=getLexeme(str.substring(0,str.length()-2));  
                if(lex!=null)  
                {  
                    result.add(lex);  
                }  
                lex=getLexeme(endstr);  
                if(lex!=null)  
                {  
                    result.add(lex);  
                }  
                  
                buffer=new StringBuffer();  
            }  
            else if(endstr.charAt(0)=='=' || endstr.charAt(0)=='>' || endstr.charAt(0)=='<' || endstr.charAt(0)=='&' || endstr.charAt(0)=='|' )  
            {  
                Lexeme lex=getLexeme(str.substring(0,str.length()-2));  
                if(lex!=null)  
                {  
                    result.add(lex);  
                }  
                lex=getLexeme(endstr.substring(0,1));  
                if(lex!=null)  
                {  
                    result.add(lex);  
                }  
                  
                buffer=new StringBuffer();  
                buffer.append(endstr.charAt(1));  
            }  
        }  
    }  
    //根据一个字符串获取词素  
    private static Lexeme getLexeme(String lex)  
    {  
        Lexeme result=null;  
        if(lex.equals(" ") || lex.equals("\t") || lex.equals("\r\n") || lex==null|| lex.length()==0)  
        {  
            return null;  
        }  
        if(lex.equals("if"))  
        {  
            result=new Lexeme(Lexeme.IF,lex);  
        }  
        else if(lex.equals("else"))  
        {  
            result=new Lexeme(Lexeme.ELSE,lex);  
        }  
        else if(lex.equals("while"))  
        {  
            result=new Lexeme(Lexeme.WHILE,lex);  
        }  
        else if(lex.equals("{") || lex.equals("}")|| lex.equals("[") || lex.equals("]") || lex.equals("(") || lex.equals(")"))  
        {  
            result=new Lexeme(Lexeme.BRACKET,lex);  
        }  
        else if(lex.equals(">") || lex.equals("<") || lex.equals("==") || lex.equals(">=") || lex.equals("<=") || lex.equals("!="))  
        {  
            result=new Lexeme(Lexeme.CPREOP,lex);  
        }  
        else if(lex.equals("&") || lex.equals("|") || lex.equals("^"))  
        {  
            result=new Lexeme(Lexeme.BITOP,lex);  
        }  
        else if(lex.equals("&&") || lex.equals("||"))  
        {  
            result=new Lexeme(Lexeme.LOGIOP,lex);  
        }  
        else if(lex.equals("+") || lex.equals("-") || lex.equals("*") || lex.equals("/"))  
        {  
            result=new Lexeme(Lexeme.ARMTOP,lex);  
        }  
        else if(isNumber(lex))  
        {  
            result=new Lexeme(Lexeme.NUMBER,lex);  
        }  
        else if(isStr(lex))  
        {  
            result=new Lexeme(Lexeme.LITERAL,lex);  
        }  
        else if(lex.equals("new"))  
        {  
            result=new Lexeme(Lexeme.NEW,lex);  
        }  
        else if(lex.equals("int") || lex.equals("char") || lex.equals("double"))  
        {  
            result=new Lexeme(Lexeme.BASETYPE,lex);  
        }  
        else if(lex.equals("class"))  
        {  
            result=new Lexeme(Lexeme.CLASS,lex);  
        }  
        else if(lex.equals("private") || lex.equals("public"))  
        {  
            result=new Lexeme(Lexeme.ACCESSFLAG,lex);  
        }  
        else if(lex.equals("static"))  
        {  
            result=new Lexeme(Lexeme.STATIC,lex);  
        }  
        else if(lex.equals("return"))  
        {  
            result=new Lexeme(Lexeme.RETURN,lex);  
        }  
        else if(lex.equals("break"))  
        {  
            result=new Lexeme(Lexeme.BREAK,lex);  
        }  
        else if(lex.equals("continue"))  
        {  
            result=new Lexeme(Lexeme.CONTINUE,lex);  
        }  
        else if(lex.equals("."))  
        {  
            result=new Lexeme(Lexeme.DOT,lex);  
        }  
        else if(lex.equals("this"))  
        {  
            result=new Lexeme(Lexeme.THIS,lex);  
        }  
        else if(lex.equals(";"))  
        {  
            result=new Lexeme(Lexeme.SEMI,lex);  
        }  
        else if(lex.equals("="))  
        {  
            result=new Lexeme(Lexeme.EQUAL,lex);  
        }  
        else  
        {  
            result=new Lexeme(Lexeme.ID,lex);  
        }  
        return result;  
    }  
    private static boolean isStr(String lex)  
    {  
        if(lex.charAt(0)!='\"' || lex.charAt(lex.length()-1)!='\"')  
            return false;  
        for(int i=1;i<=lex.length()-2;i++)  
        {  
            if(lex.charAt(i)=='\"')  
            {  
                return false;  
            }  
        }  
        return true;  
    }  
    private static boolean isNumber(String str)  
    {  
        try  
        {  
            int i=Integer.valueOf(str);  
            return true;  
        }  
        catch(Exception e)  
        {}  
        try  
        {  
            double j=Double.valueOf(str);  
            return true;  
        }  
        catch(Exception e)  
        {}  
        return false;  
    }  
    //从流中读取一个字符  
    private static boolean Read()  
    {  
        int d;  
        try {  
            d = br.read();  
            if(d==-1)  
            {  
                return false;  
            }  
            buffer.append((char)d);  
        } catch (IOException e) {  
            e.printStackTrace();  
            return false;  
        }  
         
         
        return true;  
    }  
}  

然后自己写一段程序,试一试能不能正确的解析:
[java] view plain copy
class testclass{  
   private static int j=0;  
   public int i=1;  
   public testclass()  
  {  
     double c=1;  
     char[] d="123456";  
        
  }  
  private static double func1()  
  {  
     if(j==0)  
     {  
     return 1.5  
     }  
     else  
     {  
       while(i<=10)  
       {  
         i=i+1;  
       }  
       return i;  
     }   
   }  
}  

然后看一看输出的结果:
[java] view plain copy
<14:class>  
<10:testclass>  
<3:{>  
<15:private>  
<16:static>  
<13:int>  
<10:j>  
<23:=>  
<8:0>  
<22:;>  
<15:public>  
<13:int>  
<10:i>  
<23:=>  
<8:1>  
<22:;>  
<15:public>  
<10:testclass>  
<3:(>  
<3:)>  
<3:{>  
<13:double>  
<10:c>  
<23:=>  
<8:1>  
<22:;>  
<13:char>  
<3:[>  
<3:]>  
<10:d>  
<23:=>  
<9:"123456">  
<22:;>  
<3:}>  
<15:private>  
<16:static>  
<13:double>  
<10:func1>  
<3:(>  
<3:)>  
<3:{>  
<0:if>  
<3:(>  
<10:j>  
<4:==>  
<8:0>  
<3:)>  
<3:{>  
<17:return>  
<8:1.5>  
<3:}>  
<1:else>  
<3:{>  
<2:while>  
<3:(>  
<10:i>  
<4:<=>  
<8:10>  
<3:)>  
<3:{>  
<10:i>  
<23:=>  
<10:i>  
<7:+>  
<8:1>  
<22:;>  
<3:}>  
<17:return>  
<10:i>  
<22:;>  
<3:}>  
<3:}>  
<3:}>  

貌似比较正确

2016-2-3 01:22
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 8 楼』:  7cucu: 一个易于理解的编译器 8楼

cucu: a compiler you can understand (part 1)
标签: compiler编译器
2013-01-13 20:33 1317人阅读 评论(0) 收藏 举报
分类:  编译器与原理(2)  
目录(?)[+]
译者序:
最近在学习一些编译器的基本知识,就找到了这篇英文的网志,在csdn搜了一下貌似没有人翻译,所以我干脆翻译了算了,反正都是学习。
原文地址:http://zserge.com/blog/cucu-part1.html
cucu: 一个易于理解的编译器 (part 1)
让我们来讨论一下编译器吧。你有想过自己去写一个编译器吗?
我将会让你看到这是一件多么简单的事情!但这个网志的第一部分有点偏理论,所以希望你们能够保持耐心。
我们的目标
CUCU 是一个“玩具”编译器用来编译一个“玩具”语言。我希望这个玩具语言能尽可能的像标准C语言,因此一个正确的CUCU程序同样能够使用C编译器进行编译。当然,整个C语言标准时非常复杂的,我们这里的CUCU所使用的语法只是C语言的一小部分。
比如说,这里有一个有效的CUCU程序片段:
int cucu_strlen(char *s) {
    int i = 0;
    while (s) {
        i = i + 1;
    }
    return i;
}
语法
接下来我们要定义我们这个编程语言的语法。这是一个重要的步骤,因为在设计我们编译器的时候将会依赖于这个语法。
让我们从上到下来设计语法。我们的源文件包含一个程序。什么是程序?根据经验我们可以知道,程序就是一个列表,包括变量声明、函数声明、函数定义等,比如:
int func(char *s, int len); /* function declaration */
int i;                      /* variable declaration */

int func(char *s, int len) { /* function definition */
    ...
}
让我们尝试着将它写成EBNF的形式(如果你不知道什么是EBNF也没关系,它看上去很直观):
(译者:关于EBNF的详细信息请参考http://zh.wikipedia.org/wiki/%E6 ... F%E8%8C%83%E5%BC%8F
<program> ::= { <var-decl> | <func-decl> | <func-def> } ;
这个表示法说明:一个函数是一个重复的序列,这个序列中的每一项是变量声明、函数声明或者函数定义。那么,这些所谓的声明和定义又是啥呢?让我们继续往下走。
<var-decl> ::= <type> <ident> ";"
<func-decl> ::= <type> <ident> "(" <func-args> ")" ";"
<func-def> ::= <type> <ident> "(" <func-args> ")" <func-body>
<func-args> ::= { <type> <ident> "," }
<type> ::= "int" | "char *"
因此,变量声明很简单:一个类型名加上一个标识符,然后在后面加上一个分号,就像我们经常在C语言中使用的那样。
int i;
char *s;
函数声明稍微要复杂一点,首先是“类型+标识符”,然后在括号里可以有选择性的加上 <func-args>
函数的参数表,是一个用逗号分割开的“类型+标识符”的序列,比如:
char *s, int from, int to
事实上,参数表最后的逗号在CUCU语言里是允许的,但不是必要的。之所以这么做是为了使我们分析代码变的简单。
语言所支持的类型只有int和char*,标识符是一串字母、数字或者下划线。
唯一没有说明的只有<func-body>. 但首先我们需要讨论一下语句(statements)和表达式(experssions)。
语句是指我们的语言中最小的独立元素。下面是一下有效的语句:
/* 这是一些简单的语句 */
i = 2 + 3; /* 赋值语句 */
my_func(i); /* 函数调用语句 */
return i; /*返回语句 */

/* 这是一些复合语句 */
if (x > 0) { .. } else { .. }
while (x > 0) { .. }
表达式是语句的一部分,它比语句更小。和语句不同的是,表达式总是会返回一个值。通常,表达式会是算数预算。比如在语句func(x[2], i + j)里,表达式是 x[2] 和 i+j。
因此根据上述分析,我们有:
<func-body> ::= <statement>
<statement> ::= "{" { <statement> } "}"                /* 语句块 */
                | [<type>] <ident> [ "=" <expr> ] ";"  /* 赋值 */
                | "return" <expr> ";"
                | "if" "(" <expr> ")" <statement> [ "else" <statement> ]
                | "while" "(" <expr> ")" <statement>
                | <expr> ";"
下面是一些CUCU语言中可行的表达式:
<expr> ::= <bitwise-expr>
           | <bitwise-expr> = <expr>
<bitwise-expr> ::= <eq-expr>
                   | <bitwise-expr> & <eq-expr>
                   | <bitwise-expr> | <eq-expr>
<eq-expr> ::= <rel-expr>
              | <eq-expr> == <rel-expr>
              | <eq-expr> != <rel-expr>
<rel-expr> ::= <shift-expr>
               | <rel-expr> < <shift-expr>
<shift-expr> ::= <add-expr>
                 | <shift-expr> << <add-expr>
                 | <shift-expr> >> <add-expr>
<add-expr> ::= <postfix-expr>
               | <add-expr> + <postfix-expr>
               | <add-expr> - <postfix-expr>
<postfix-expr> ::= <prim-expr>
                   | <postfix-expr> [ <expr> ]
                   | <postfix-expr> ( <expr> { "," <expr> } )
<prim-expr> := <number> | <ident> | <string> | "(" <expr> ")"
注意到递归定义的表达式了吗?除此之外这些表达式还说明了运算符的优先级,从下到上优先级以此降低:括号和方括号的优先级较高,而赋值的优先级较低。
例如,根据语法定义,表达式 8>>1+1 的运算顺序将会是  8>>(1+1)), 而不会是 (like in (8>>1)+1), 因为 >> 的优先级要低于 +.
词法分析器
当我们解决了语法问题,我们差不多可以开始了。第一件事是做一个词法分析器。我们的编译器使用一个字节流作为输入,而词法分析器的作用就是将这个字节流分割成更小的符号(token),以便于后续的处理。词法分析器为我们提供了某种程度的抽象使得之后的解析器得以简化。
例如,一个字节序列 "int i = 2+31;"将会分成以下的符号:
int
i
=
2
+
31
;
在一个普通的词法分析器中,一个词素是一个由类型和值组成的二元组。因此,相对于以上的列表,我们更期望能得到一个如下的二元组<TYPE:int>,<ID:i>, <ASSIGN:=>,<NUM:2>,<PLUS:+>,<NUM:31>,<SEMI:;>。为了简便我们现在是通过值来反推类型,当然这是非常不严谨的。
词法分析器的主要问题是一旦一个字节从流中读取了之后,它就再也不能被重新放回流中。因此,如果我们读到了一个字节,而这个字节不能被加入到当前的符号中,这时候应该怎么办呢?我们应当把这个字节存到哪里,等待当前符号处理完成之后再去处理这个字节呢?
事实上,几乎任何词法解析器都有预读的机制。我们的语法很简单,因此我们只需要一个字节 - nextc当缓冲区就足够了。它存储一个从流中读取出来的但还没有被加入到当前符号中的字节。
另外,我必须在这里提个醒- 我在CUCU的代码的词法分析器中使用了很多全局变量。我知道这是个不好的习惯,但如果我把所有的变量都作为函数参数的话,这个编译器的代码看起来就不是那么的简洁了。
词法解析器的全部就是一个函数 readtok() 。而它的算法也很简单:
跳过开头的所有空格
尝试读取一个标识符(一个字母、数字以及下划线的序列)
如果发现不是一个标识符,尝试读取一些运算符,比如 &, |, <, >, =, !.
如果不是运算符,尝试读取字符串文本,比如"...." 或者 '....'
如果仍然失败,或许是一个注释,比如 /* ... */
如果继续失败,尝试读取一个字节,或许是括号之类的字符,比如 "("或者 "["。
#include <stdio.h> /* for vpritnf */
#include <stdarg.h> /* for va_list */
#include <stdlib.h> /* for exit() */
#include <ctype.h> /* for isspace, isalpha... */

#define MAXTOKSZ 256
static FILE *f; /* input file */
static char tok[MAXTOKSZ];
static int tokpos;
static int nextc;

void readchr() {
    if (tokpos == MAXTOKSZ - 1) {
        tok[tokpos] = '\0';
        fprintf(stderr, "token too long: %s\n", tok);
        exit(EXIT_FAILURE);
    }
    tok[tokpos++] = nextc;
    nextc = fgetc(f);
}

void readtok() {
    for (;;) {
        while (isspace(nextc)) {
            nextc = fgetc(f);
        }
        tokpos = 0;
        while(isalnum(nextc) || nextc == '_') {
            readchr();
        }
        if (tokpos == 0) {
            while (nextc == '<' || nextc == '=' || nextc == '>'
                    || nextc == '!' || nextc == '&' || nextc == '|') {
                readchr();
            }
        }
        if (tokpos == 0) {
            if (nextc == '\'' || nextc == '"') {
                char c = nextc;
                readchr();
                while (nextc != c) {
                    readchr();
                }
                readchr();
            } else if (nextc == '/') {
                readchr();
                if (nextc == '*') {
                    nextc = fgetc(f);
                    while (nextc != '/') {
                        while (nextc != '*') {
                            nextc = fgetc(f);
                        }
                        nextc = fgetc(f);
                    }
                    nextc = fgetc(f);
                }
            } else if (nextc != EOF) {
                readchr();
            }
        }
        break;
    }
    tok[tokpos] = '\0';
}

int main() {
    f = stdin;
    nextc = fgetc(f);

    for (;;) {
        readtok();
        printf("TOKEN: %s\n", tok);
        if (tok[0] == '\0') break;
    }
    return 0;
}
如果我们把一个C语言的源文件作为这个词法分析器的输入,它将会输出一个符号的列表,每个符号一行。
搞定,让我们稍微休息一下,接着进入第二部分。

cucu: a compiler u can understand (part 2)
2013-01-14 22:08 748人阅读 评论(0) 收藏 举报
分类:  编译器与原理(2)  
目录(?)[+]
原文地址:http://zserge.com/blog/cucu-part2.html
到目前为止,我们已经定义了我们语言的语法并编写了一个词法分析器。在本篇文章中,我们将为我们的语言写解析器。但在开始之前,我们先需要一些辅助函数:
int peek(char *s) {
    return (strcmp(tok, s) == 0);
}

int accept(char *s) {
    if (peek(s)) {
        readtok();
        return 1;
    }
    return 0;
}

int expect(char *s) {
    if (accept(s) == 0) {
        error("Error: expected '%s'\n", s);
    }
}
peek() 函数若下一个符号与传入的字符串相等,则返回非零值。 accept()函数读取下一个符号,如果其与传入参数相同,否则返回0。expect() h帮助我们检查语言的语法。
较为困难的部分
从语言的语法中我们可以得知,语句和表达式是相互掺杂在一起的。因此这就意味着一旦我们开始写解析器,我们必须时刻记住这些递归生成规则。让我们从顶至底来进行分析。下面是最高层的函数compiler():
static int typename();
static void statement();

static void compile() {
    while (tok[0] != 0) { /* until EOF */
        if (typename() == 0) {
            error("Error: type name expected\n");
        }
        DEBUG("identifier: %s\n", tok);
        readtok();
        if (accept(";")) {
            DEBUG("variable definition\n");
            continue;
        }
        expect("(");
        int argc = 0;
        for (;;) {
            argc++;
            typename();
            DEBUG("function argument: %s\n", tok);
            readtok();
            if (peek(")")) {
                break;
            }
            expect(",");
        }
        expect(")");
        if (accept(";") == 0) {
            DEBUG("function body\n");
            statement();
        }
    }
}
这个函数首先尝试读取类型名,其次是标识符。如果在此之后紧跟一个分号,则说明是一个变量声明。如果跟着括号,说明是一个函数。如果是函数,就接着去逐一搜索参数,再次之后如果没有分号,则说明是一个函数定义(有函数体),否则就只是一个函数声明(只有函数名和类型)。
这里, typename() 是一个用来让我们跳过类型名的函数。 我们指接受int类型、char类型以及其指针(char*):
static int typename() {
    if (peek("int") || peek("char")) {
        readtok();
        while (accept("*"));
        return 1;
    }
    return 0;
}
最有趣的大概就是 statement() 函数了。它可以分析一个单独的语句,而这个语句可以是一个块、一个局部变量的定义/声明、一个return语句等。
现在让我们来看看它的样子:
static void statement() {
    if (accept("{")) {
        while (accept("}") == 0) {
            statement();
        }
    } else if (typename()) {
        DEBUG("local variable: %s\n", tok);
        readtok();
        if (accept("=")) {
            expr();
            DEBUG(" :=\n");
        }
        expect(";");
    } else if (accept("if")) {
        /* TODO */
    } else if (accept("while")) {
        /* TODO */
    } else if (accept("return")) {
        if (peek(";") == 0) {
            expr();
        }
        expect(";");
        DEBUG("RET\n");
    } else {
        expr();
        expect(";");
    }
}
如果遇到的是一个“块”,即{...}的部分,就继续尝试在块中解析语句直到块结束。如果以变量名开头,则说明是一个局部变量的定义。条件语句(if/then/else)和循环语句在这里没有列出,留给读者去思考根据我们的语法,这些部分应当如何去实现。
当然,大部分语句里都包含着表达式,因此我们需要写一个函数取分析表达式。表达式解析器是一个向下递归的解析器,因此很多的表达式解析函数会互相调用直到找到主表达式为止。所谓的主表达式,根据我们的语法,是指一个数字(常量)或者一个标识符(变量或者函数)。
static void prim_expr() {
    if (isdigit(tok[0])) {
        DEBUG(" const-%s ", tok);
    } else if (isalpha(tok[0])) {
        DEBUG(" var-%s ", tok);
    } else if (accept("(")) {
        expr();
        expect(")");
    } else {
        error("Unexpected primary expression: %s\n", tok);
    }
    readtok();
}

static void postfix_expr() {
    prim_expr();
    if (accept("[")) {
        expr();
        expect("]");
        DEBUG(" [] ");
    } else if (accept("(")) {
        if (accept(")") == 0) {
            expr();
            DEBUG(" FUNC-ARG\n");
            while (accept(",")) {
                expr();
                DEBUG(" FUNC-ARG\n");
            }
            expect(")");
        }
        DEBUG(" FUNC-CALL\n");
    }
}

static void add_expr() {
    postfix_expr();
    while (peek("+") || peek("-")) {
        if (accept("+")) {
            postfix_expr();
            DEBUG(" + ");
        } else if (accept("-")) {
            postfix_expr();
            DEBUG(" - ");
        }
    }
}

static void shift_expr() {
    add_expr();
    while (peek("<<") || peek(">>")) {
        if (accept("<<")) {
            add_expr();
            DEBUG(" << ");
        } else if (accept(">>")) {
            add_expr();
            DEBUG(" >> ");
        }
    }
}

static void rel_expr() {
    shift_expr();
    while (peek("<")) {
        if (accept("<")) {
            shift_expr();
            DEBUG(" < ");
        }
    }
}

static void eq_expr() {
    rel_expr();
    while (peek("==") || peek("!=")) {
        if (accept("==")) {
            rel_expr();
            DEBUG(" == ");
        } else if (accept("!=")) {
            rel_expr();
            DEBUG("!=");
        }
    }
}

static void bitwise_expr() {
    eq_expr();
    while (peek("|") || peek("&")) {
        if (accept("|")) {
            eq_expr();
            DEBUG(" OR ");
        } else if (accept("&")) {
            eq_expr();
            DEBUG(" AND ");
        }
    }
}

static void expr() {
    bitwise_expr();
    if (accept("=")) {
        expr();
        DEBUG(" := ");
    }
}
上面是一大段的代码,但是不需要感到头疼,因为它们都简单的很。每一个分析表达式的函数首先都尝试调用一个更高优先级的表达式分析函数。接着,如果找到了这个函数期望的符号,则它继续调用高优先级的函数。然后当它分析完了一个二元表达式(如x+y、x&y、x==y)的两部分之后,就将值返回。有些表达式可以链式连接(如a+b+c+d),因此需要循环的分析它们。
我们在分析每一个表达式的时候都会输出一些调试信息,这些信息会给我们带来一些有趣的结果。例如,若我们分析以下代码片段:
int main(int argc, char **argv) {
    int i = 2 + 3;
    char *s;
    func(i+2, i == 2 + 2, s[i+2]);
    return i & 34 + 2;
}
我们将会得到如下的输出:
identifier: main
function argument: argc
function argument: argv
function body
local variable: i
const-2  const-3  +  :=
local variable: s
var-func  var-i  const-2  +  FUNC-ARG
var-i  const-2  const-2  +  ==  FUNC-ARG
var-s  var-i  const-2  +  []  FUNC-ARG
FUNC-CALL
var-i  const-34  const-2  +  AND RET
所有的表达式都会被写成逆波兰式(比如2+3变成23+)。而这对于有堆栈的计算机来说,是更为方便合理的形式,当操作数在栈顶的时候,函数能够执行出栈操作并取得操作数,之后将结果压栈。
虽然对于现在的以寄存器为基础的CPU,这或许不是一个最优的方法,但这个方法很简单并且能够满足我们编译器的需要。

symbols
现在,我们已经完成了很多工作了,我们使用不到300行的代码写了一个词法分析器和解析器。接下来我们要做的事情是添加以下函数,以便让这些符号(比如变量名、函数名)能够正确的工作。一个编译器应该有一个符号表以便能够很快的找到这些符号的地址,所以当你在代码中写“i=0"的时候,实际上你是将0这个值放入了内存的0x1234的位置(假设变量i在内存的位置就是0x1234)。相似的,当我们调用函数”func()"时,实际上做的是跳转到0x5678继续执行而已(假设func这个符号的的值是0x5678)。
我们需要一个数据结构来存放符号:
struct sym {
    char type;
    int addr;
    char name[];
};
这里type 有不同的含义。 我们用一个单独的字母来标示不同的类型:
L - 局部变量。 addr 存储变量在堆栈里的地址
A - 函数参数。 addr 存储参数在堆栈里的地址
U - 未定义的全局变量。 addr 存储其在内存中的绝对地址。
D - 定义过的全局变量。 其余同上。

So far, I've added two functions: sym_find(char *s) to find symbol by its name, andsym_declare() to add a new symbol.
到此为止,我们还需要增加两个函数:: sym_find(char *s) 来根据符号名查找符号, sym_declare() 来加入一个新的符号。
现在我们已经可以去设计后端的架构了,详情见下篇文章。
如果你忘了前面的信息你可以到part1部分去查阅。


cucu: a compiler u can understand (part 3)
2013-01-28 15:04 824人阅读 评论(0) 收藏 举报
分类:  编译器与原理(2)  
目录(?)[+]
现在让我们谈谈编译器的后端架构。C语言应该是一个可以移植的语言,但是在移植的过程中,我们并没有必要为新的CPU架构去重新编写整个C的编译器。编译器后端用来产生低级别字节码,而编译器前端会调用编译器后端的函数。一个好的后端设计会使得编译器具有良好的移植性。
我希望CUCU成为一个可以移植的编译器(也就是所谓的交叉编译)。因此我打算将后端代码写到一个独立的模块里。
但在我们具体考虑一个后端代码之前,我们还有很多工作要做。
简化的cpu架构
我们间滑过的CPU架构含有两个寄存器(我们记作A和B)和一个栈。寄存器A是一个累加器。像很多RISC的CPU一样,我们将使用定长指令集,为了更加有趣一些,我们并不把指令变成16进制代码,而采用较为自然的方式呈现。
我使用一种简单的方式设计指令。每一个指令8字节长(的确,这有点长,但是没有关系,毕竟这是一个假象的架构)。开头的7个字节是ASCII的符号,最后一个是0x10('\n')。
这就让我们可以设计出更易于阅读的指令,比如A:=A+B, A:=1ef8,或 push A。这些指令基本上都是自解释的了(“将B寄存器的内容加给A寄存器”,“将0x1ef8放入A寄存器”和“将寄存器A的值压入堆栈”)。
A:=NNNN - 将0xNNNN放入寄存器A。
A:=m[A] - 将地址为寄存器A中的值处的内容(作为字节)存入寄存器A中
A:=M[A] -将地址为寄存器A中的值处的内容(作为int型变量)存入寄存器A中
m[B]:=A - 将寄存器A中的值存入寄存器B所指向的地址处(作为字节)。
M[B]:=A - 将寄存器A中的值存入寄存器B所指向的地址处(作为int型变量)。
push A - 将寄存器A中的值压入队长。
pop B - 将栈顶元素出栈并放入寄存器B。
A:=B+A - 将A和B的值相加并将结果放入A。
A:=B-A - B减A并将结果存入A。
A:=B&A - 按位与。
A:=B|A - 按位或。
A:=B!=A -若B!=A,则A为1,否则A为0.
A:=B==A - 若B==A,则A为1,否则A为0
A:=B<<A - 将B中的值左移A位并将结果存入A。
A:=B>>A - 将B中的值右移A位并将结果存入A。
A:=B<A - 若B<A ,则A为1,否则A为0.
popNNNN - 将栈中的NNNN个元素出栈。
sp@NNNN - 将栈中地址是NNNN的元素的值放入寄存器A。
jmpNNNN - 程序跳转到NNNN地址处继续执行。
jpzNNNN - 若A的值是0,则跳转到NNNN处执行。
call A - 调用地址存在A中的函数。
ret - 从函数中返回
cucu后端架构设计
当我们包含“gen.c” 这个文件时,实际上就是包含了一个后端架构的具体实现。让我们从两个最基本的函数开始:: gen_start() 和 gen_finish(). 这两个函数用来生成程序头(比如PE头和ELF头)和一些预处理过的字节码。
编译器使用一个函数 emit(), 来将字节码发射到code[]数组中。这个数组的每一个元素都代表着一个可以使用的编译好的程序。
因此,编译器只调用后端架构提供的借口,而后端架构调用emit()来生成特定的字节码,这就是编译器编译出机器语言的过程。
因此,现在我们需要定义出最常用的一些指令,然后让后端架构去实现。让我们从一个最简单的程序开始。
int main() {
    return 0;
}
让我们分析下函数调用的过程。这个过程也就是函数参数如何传递给函数体以及返回值如何处理的过程。我们在前面也已经说过了,参数是放在栈顶进行传递的(第一个参数第一个压栈)。让我们再做个约定,寄存器A带有函数的返回值。
事实上,我们使用寄存器A来存储所有的值,寄存器B只用来存储临时变量。
对于上述程序,我们期待的字节码应该有如下的形式:
A:=0000
ret
因此我们需要一个函数来将立即数存入寄存器A,还需要一个函数用来处理返回。我们把这两个函数定义为gen_const(int)和gen_ret()。
    当编译器发现一个主表达式是立即数的时候,gen_const就会被调用,当发现一个return语句时,gent_ret就会被调用。虽然,有些函数的类型是void,因此其没有显式的Return。但为了安全和简单,在每一个函数的结尾,我们都会去调用一次gen_ret(),即使其前面有一个显式的return。
我们的编译器并不追求优化、效率和安全,因此这种双return的方式对于我们是可行的。
数学运算
现在让我们来编译数学表达式。这些数学表达式都很相似,因此我们使用一个例子来说明编译器是如何处理的。还记得词法分析器如何工作吗?它分析(更严谨的说法是编译)表达式左值,表达式右值然后才是运算符。
这就是一个典型的数学表达式编译的过程(还记得把大象装进冰箱的笑话吗):
..计算左值
push A
..计算右值
pop B
A:=A+B
当我们计算完左值的时候我们需要暂存结果。使用堆栈是一个很好的选择。因此一个表达式1+2+3我们将会编译成如下的形式:
A:=0001  -+     -+
push A    |      |
A:=0002   | 1+2  |
pop B     |      |
A:=A+B   -+      | +3
push A           |
A:=0003          |
pop B            |
A:=A+B       ----+
一些其它的东西
处理符号也同样很简单。
为了调用一个函数,我们首先要把其地址放入寄存器A,然后使用gen_call()产生代码call A。
要访问局部变量则使用gen_stack_addr然后返回这个变量在堆栈中的地址。
访问全局变量则使用gen_sym_addr()。除此之外,每次建立一个新的符号编译器就需要产生一些代码(比如汇编代码),gen_sym用于处理这些情况。
gen_pop 从堆栈顶弹出N个元素,同时增加栈顶指针。
gen_unref用于产生一些指针相关操作。根据类型的不同(byte或者int),会产生A:=m[A] or A:=M[A] 代码。
gen_array将一个数组地址压入栈顶。
最后,当遇到if/while语句的时候,gen_patch用于追加产生地址跳转的代码。为什么说是追加呢?因为当我们遇到需要跳转的语句时需要跳转的地址是未知的,这个地址依赖于编译后的语句块的大小。因此需要在语句块编译结束后进行追加地址跳转的代码。
差不多要成功了,让我们试试以下的程序:
int main() {
    int k;
    k = 3;
    if (k == 0) {
        return 1;
    }
    return 0;
}

jmp0008 # 由gen_start()产生,跳转到main,地址为0x08
push A  # 为局部变量K申请空间
sp@0000 # 取得刚才申请的空间的地址
push A  # 将这个地址入栈
A:=0003 # 将3存入A里
pop B   # 取得之前存入的K的地址
M[B]:=A # 将A中的值作为int放入K中
sp@0000 # 取得K的地址
A:=M[A] # 取得其中的值作为int存入A
push A  # 存这个值
A:=0000 # 将A的值置0
pop B   # 取得之前存入的K的值
A:=B==A # 比较A和B的值 (也就是"k" 和 0)
jmz0090 # 如果是假(A!=B, k!=0) - 跳转到 to 0x90
A:=0001 # 把1放入A中作为返回值
pop0001 # 释放堆栈中存储k的值的空间
ret     # return
jmp0090 # else分支内容在此,下一条语句地址是0x90
A:=0000 # 把0放入A中作为返回值
pop0001 # 释放堆栈中存储k的值的空间
ret     # return
ret     # 之前为了安全考虑的第二次return
虽然我们的代码又乱又臃肿,但它的确能工作。更重要的是,你现在能弄明白编译器的工作原理并且可以自己动手做一个自己的编译器。
但是,我必须警告你。。。
警告
请千万不要按以上的步骤那么做!如果你要写一个自己的编译器,建议使用以下成熟的工具:
flex/lex/jlex/...
yacc/bison/cup...
ANTLR
Ragel
and many others
除此之外,你想要一些专业的文献,比如龙书(《编译原理》,译者注)。并且coursera.org上的课程或许对你会有帮助。
如果你需要使你的系统可以适应现有的语言,你可以去了解LLVM的后端和GCC的后端。
如果你需要一些更多地关于玩具编译器的信息,可以去了解一下SmallC。
如果你想写一个简单的语言编译器,可以去了解一下PL/0或者Basic或者C。
但是请千万不要去从头写一个编译器并把它用在实际的工作中。
后记
整个项目的代码可以在这里找到。授权给MIT,任何人都可以免费使用或者修改。
不管如何,编译器是个很有趣的东西。我希望你能喜欢它。

2016-2-3 01:37
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 9 楼』:  8 kickout 转贴李家芳的《硬盘分区表详解 》 9楼

kickout

转贴李家芳的《硬盘分区表详解 》
http://www.cn-dos.net/forum/viewthread.php?tid=115&fpage=174

硬盘主引导扇区 = 硬盘主引导记录(MBR)+ 硬盘分区表(DPT)
--------------------------------------------------------------
物理位置:0面0道1扇区(clindyer 0, side 0, sector 1)
大小: 512字节
其中:MBR 446字节(0000--01BD),DPT 64字节(01BE--01FD),结束标志2字节(55 AA)
功能:MBR通过检查DPT分区信息引导系统跳转至DBR;
读取: 使用NORTON DISKEDIT, 在OBJECT菜单中选择DRIVE——>PHYSICAL DISK-—HARD DISK, 然后, 在OBJECT菜单中选择DISK PARTITION TABLE即可读取, 并使用TOOLS菜单中的 WRITE OBJECT TO 选项存入指定文件备份;
写入: 使用NORTON DISKEDIT, 在OBJECT菜单中选择DRIVE——>FLOOPY DISK, 选择备份的DPT文件, 然后使用TOOLS菜单中的WRITE OBJECT TO——>PHYSICAL SECTOR 选项写入 001 (clindyer 0, side 0, sector 1);

详解:
000H--08AH MBR启动程序(寻找引导分区)
08BH--0D9H MBR启动字符串
0DAH--1BCH 保留("0")
1BEH--1FDH 硬盘分区表
1FEH--1FFH 结束标志(55AA)


活动分区引导扇区(DBR)
--------------------------
物理位置:1面0道1扇区(clindyer 0, side 1, sector 1)
大小: FAT16 1扇区 512字节 FAT32 3扇区 1536字节
功能:包含机器CMOS等信息(0000--0059), 核对该信息并引导指定的系统文件, 如NTLDR等;
读取: 使用NORTON DISKEDIT, 在OBJECT菜单中选择DRIVE——>LOGICAL DISK-—DISK C,
然后, 在OBJECT菜单中选择BOOT RECORD即可读取, 并使用TOOLS菜单中的
WRITE OBJECT TO 选项存入指定文件备份;
写入: 使用NORTON DISKEDIT, 在OBJECT菜单中选择DRIVE——>FLOOPY DISK, 选择备份的DBR文件, 然后使用TOOLS菜单中的WRITE OBJECT TO——>PHYSICAL SECTOR 选项写入011 (clindyer 0, side 1, sector 1);


详解:

000H--002H 3 BYTE的跳转指令(去启动程序, 跳到03EH)
003H--03DH BIOS参数区
03EH--19DH DOS启动程序
19EH--1E5H 开机字符串
1E6H--1FDH 文件名(IO.SYS, MSDOS.SYS)
1FEH--1FFH 结束标记(55AA)


硬盘分区表(DPT)
---------------------
偏移地址 字节数 含义分析

01BE 1 分区类型:00表示非活动分区:80表示活动分区;其他为无效分区。

01BF~01C1 3 *分区的起始地址(面/扇区/磁道),通常第一分区的起始地址开始于1面0道1扇区,因此这三个字节应为010100

01C2 1 #分区的操作系统的类型。

01C3~01C5 3 *该分区的结束地址(面/扇/道)

01C6~01C9 4 该分区起始逻辑扇区

01CA~01CD 4 该分区占用的总扇区数

注释: * 注意分区的起始地址(面/扇区/磁道)和结束地址(面/扇/道)中字节分配:

00000000 01000001 00010101
^^^^^^^^ ==~~~~~~ ========

^ 面(磁头) 8 位
~ 扇区 6 位
= 磁道 10 位

# 分区的操作系统类型(文件格式标志码)

4---DOS FAT16 32M
7---NTFS(OS/2)
83---LINUX>64M


DPT 总共64字节(01BE--01FD), 如上所示每个分区占16个字节, 所以可以表示四个分区, 这也就是为什么一个磁盘的主分区和扩展分区之和总共只能有四个的原因.


逻辑驱动器
-----------
扩展分区的信息位于以上所示的硬盘分区表(DPT)中, 而逻辑驱动器的信息则位于扩展分区的起始扇区, 即该分区的起始地址(面/扇区/磁道)所对应的扇区, 该扇区中的信息与硬盘主引导扇区的区别是不包含MBR, 而16字节的分区信息则表示的是逻辑驱动器的起始和结束地址等.


所以, 在磁盘仅含有一个主分区, 一个扩展分区(包含多个逻辑驱动器)的情况下, 即使由于病毒或其他原因导致硬盘主引导扇区的数据丢失(包括DPT), 也可以通过逻辑驱动器的数据来恢复整个硬盘.

例如: 以下是一个硬盘的分区情况.

道 面 扇 道 面 扇 起始扇(逻辑) 结束扇 总共扇区
MBR 0 0 1 - - - - - -
C 0 1 1 276 239 63 63 4,188,239 4,188,177
扩 277 0 1 554 239 63 4,188,240 8,391,599 4,203,360
D 277 1 1 554 239 63 4,188,303 8,391,599 4,203,297


如果主分区表损坏, 则可以通过手工查找扩展分区表中所包含的逻辑驱动器数据, 在本例中就是D盘所对应的数据, 然后将其起始扇(逻辑)减去63就是所对应的扩展分区的起始扇(逻辑), 将其起始地址(面/扇区/磁道)改为0面就是扩展分区的起始地址. 然后通过扩展分区就可以得到主分区C的信息, 然后就可以使用DISK/MBR命令和手工填写分区表恢复整个硬盘.

实际使用这种方法比较麻烦, 如果知道每个分区的大小, 则可以通过使用 PQ MAGIC 5 将磁盘重新分区为原来大小(注意: 千万不能应用, 我们只是通过它来获得数据), 并查看INFO来获得以上数据, 记录以后取消该分区操作, 然后使用NORTON DISK 2000手工修改DPT表, 恢复整个硬盘.

该例所对应的分区表数据:
                                          80 01
01 00 06 EF 7F 14 3F 00 00 00 11 E8 3F 00 00 00
41 15 05 EF BF 2A 50 E8 3F 00 60 23 40 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA

扩展分区表数据:
                                          00 01
41 15 07 EF BF 2A 8F E8 3F 00 21 23 40 00

注意: 逻辑起始扇区和总共分区数是左边为低位, 如该例的扩展分区的起始地址为50 E8 3F 00转换十进制时要先变为00 3F E8 50, 总共占用分区数60 23 40 00要先变为00 40 23 60, 同理当手工填写该值时也要进行高低位转换.



================================= kickout
大功告成,打个Kiss!

2016-2-3 12:08
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 10 楼』:  9不可思议的物理——与加来道雄对话 10楼

《被禁止的知识》试读:第一章 不可思议的物理——与加来道雄对话
加来道雄博士是纽约城市大学(City University of New York)的一位教授。他是一个非常出色的物理学家,也是一位极好的老师,被誉为当今的“爱因斯坦”(Einstein)。他是超弦理论的创立者之一。他写过不少书,也在一些记录片中把“新”物理的复杂理论解释为通俗易懂的概念。

□加来博士,我面前放着你所有的书……请你谈谈知识的价值吧!感谢你为人类觉醒所做的一切。
对话开始前,我想说说自己是如何找到你的。一年前,我正在看探索发现频道(Discovery Channel)。我被一个节目吸住目光,然而不巧的是,节目已经播出一会了,再现了一个行走在商场的极其痛苦的女人的故事。人们从她身体上径直穿过,而她对此完全不知情,她只有一种强烈的感觉,知道自己不知怎的“隐形”了。镜头转向了你,你站在时代广场(Times Square)中央,解释其他维度的存在,你说记录片中的人们其实身处另一个维度。
你能详细地解释一下维度融合的世界吗?

可以。首先,我在旧金山(San Francisco)长大,小时候,我常常连续几个小时站在日本茶园中,观看鲤鱼游泳。我常常幻想自己就是一条鱼,一条在这个浅浅的池塘中游泳的小鱼。我冥思苦想,它们是如何生活在二维世界中的:它们只能向前、向后,或者向左、向右。那么,无论哪一条鱼敢于谈论“上层”的世界,即三维世界,或是多维空间,它就会被视为“疯子”或“白痴”。我想象那里有一条科学家鱼,它会说:“呸,骗子!没有‘上面’的世界。这里只有你看到的世界;你在池塘中看到的就是存在。就是这么回事。”
所以我那时想潜到水下,逮住那条科学家鱼,将它带到“上层”世界来——三维世界、多维空间。它会看到什么呢?它将看到一个奇妙的世界,那里的生物没有鱼鳍……一个新的物理学规律,这些生物离开水也能呼吸……一个全新的生物学规律。无论大家相不相信,今天,我们物理学家认为,并在试图证明,我们就是那条鱼。我们把自己的一生都消耗在这个三维空间中(向前、向后、向左、向右、向上、向下)。有人敢于提出存在看不见的世界,一个我们无法看到、触摸的世界。这些人就被视为疯子。

□顺便说一下,我就是其中之一。在我这一生中,我的那些在未知世界的深刻经历,一直遭到蔑视和讥讽。

不会再有人嘲笑你了。当今潮流已经发生了巨大的变化。如今,物理学家相信,事实上,我们也许可以考虑更高维度的存在,即我们看不到的维度,而这些维度就在我们身边,就好像对于鱼来说的“上层”世界。

□你说的“多维空间”是什么意思?你在书中提到了十维,意为现实的“分层”。我很好奇,你为什么把自己局限于十维空间中,又是如何得出这些结论的?

爱因斯坦曾有一个梦想,他花了30年去追求这个梦想——创造一个“万有理论”(Theory of Everything),一个有一英寸长的方程式,将宇宙中的所有作用力归纳在内,让他读懂上帝的想法。然而,每当我们将重力方程式、光的方程式、比率方程式合并到一起时就会发现,在三维世界里没有足够的空间。即没有足够的空间将所有的作用力填塞在一起。然而,如果你假设存在一个更高的维度,事实上,可以假设到十维度,那么所有的作用力都会完美优雅地落到一个简洁的关联理论中。这将震惊整个物理界。但是爱因斯坦走得不够远。他在四维度就止步了,而你如果走到六维度、七维度、八维度……一直走到十维度,那么可以简单又完美地描述这些更高维度的世界。
现在,我们假设宇宙是一个类似肥皂泡泡的东西。我们生活在肥皂泡的表面,无法离开。我们被困在这里,就好像苍蝇被黏在苍蝇贴上,而肥皂泡正在膨胀——这一点可以用我们从卫星上获得的很多数据来证明。但是我们至今仍相信还有其他肥皂泡存在,那些肥皂泡上漂着的是什么?那些肥皂泡会膨胀成什么样子?如果宇宙膨胀了,会变成什么样子?
我们认为它将会变成更高的维度空间。这令我非常高兴。当我长大时,我去了主日学校。在那里,我学习了《创世纪》(Genesis)和宇宙的起源。但我的父母都是佛教徒,他们相信涅槃,涅槃既不是起源也不是末日。现在,我们物理学家认为,我们可以将佛教与犹太教和基督教中的“创世纪”融合在一起。
我们的确相信,宇宙起源于一场爆炸,但是这些大爆炸却每时每刻都在发生。它们发生在一个更为广阔的涅槃之海中,我们目前甚至给它命名为“十维超空间”。我们认为,我们有一张漂亮的图片,上面描绘了宇宙起源、成长、膨胀以及其他已经形成的宇宙,那些平行的世界,以及一个更为广阔的永恒的领域:涅槃——佛教中的涅槃;物理学上的涅槃;十维超空间。

□那么,你说的是我们可能会接受这些概念:那就是一切,那从来就是一切,那永远将是一切。大爆炸将成为一切永恒存在的证明?

说对了。人们很难将大爆炸和一些神学理论相协调,然而这张大图片,这张多元宇宙的图片,呈现了诸多过去模式的漂亮、完美的和谐。无论你信不信,这张图片与卫星数据相吻合。
我们目前有绕地卫星,即WMAR。卫星显示宇宙年龄为137亿年,展示了所有和图片相吻合的数据:我们手中的这张图片——泡泡们,就像来自一场泡泡浴;设想一次泡泡浴或是一个跳出来的、不断膨胀的泡泡,一些是小泡泡,一些是与其他泡泡相撞的泡泡。这就是从现实中创造出的新图景。纵观历史,我们可以在宗教中看到这一图景。

□这一图景是如何与宗教产生联系的呢?

如果你看一看《创世纪》的第一章第一篇,就会看到在上帝创造宇宙的时候,书中提到的瞬间。这与宇宙大爆炸理论是一致的。事实上,曾经有一位天主教大主教说,宇宙大爆炸理论与“创世纪”是能相提并论的。但后来,我们有了印度教和佛教的一些模型,宣称宇宙没有起源,也没有结束。宇宙是永恒的,所以我们有了涅槃。因而,我们不仅仅看到了两种观点的融合——宇宙大爆炸发生在涅槃之中,也看到了大爆炸一直在发生。
甚至在我们说话的瞬间,很多宇宙诞生了,我们的卫星数据与这张图片吻合——永恒的大爆炸,永恒的膨胀。事实上,我的一个朋友艾伦•古斯(Alan Guth)也许会因这张图片与卫星数据相吻合而获得诺贝尔(Nobel)物理奖。我认为,我们有一门非常好的综合科学,它来自卫星数据、爱因斯坦的统一场论以及宗教理论框架。

□由于你出色的工作,让普通人也能够理解物理学领域的复杂概念,我们对此表示衷心感谢,因为这些概念实在难以掌握。你可以给我们简单说说超弦理论(你被认为是该理论的创立者之一),以及这一理论是如何与量子物理产生联系的?

两千多年前,毕达哥拉斯(Pythagoreans)哲学派的希腊哲学家们对希腊的七弦竖琴进行分析,希望由此找到音乐的数学比率:和声数学。我们为什么有升半音和降半音,三分音符和五分音符,以及大三和弦和小三和弦?利用希腊人对它们的数学理解,得出的结论是,音乐遵循了数学规律。当他们意识到这一点时,大为震惊,由此他们认为:或许也可以用音乐语言来解释宇宙。然而,他们失败了。那时,他们不知道原子,也还没有发展出化学和物理学,这些概念还不为人知。
现在,我们已经圆满地完成了任务。我们有太多的离子、电子和中微子;我们有伽马射线和希格斯(Higgs)玻色子粒子。为了获得加州大学(University of California)的博士学位,我不得不记住成千上百个亚原子的名字和它们的外国名字。
今天我们相信,所有的粒子只不过是音符——在橡皮筋上震颤的音符。如果我用一个显微镜来观察电子,会看到它完全不是一个点(那是一张旧照片),不过是一根震颤的橡皮筋。如果我改变振动的频率,它就会变成中微子;如果我再改变,它会变成和声。我又一次改变频率,它会变成上百个我为了获得物理学博士学位而记下的亚原子之一。
我希望,将来申请物理学博士学位时,你只要说“超弦理论”就能拿到博士学位。如今我们物理学家认为,物理只不过是振动弦的音乐。化学是在这些振动着的琴弦上你弹出的旋律。宇宙是这些振动弦的交响乐,那么,上帝的想法是什么呢?
我刚刚说到,爱因斯坦花了30年时间去追求一种能够让他读懂上帝的理论。不管我们信不信,我们现在已经有了另一个候选人,一个能够读懂上帝的候选人,他已经让整个物理界为之振奋。《时代》(Time)杂志、音乐杂志以及所有的主流网络写了很多这类故事——上帝的想法。我们物理学家认为,它是通过超空间共鸣的宇宙的音乐,那就是我们认为的上帝的想法。

□出于这一原因,我被你的工作深深吸引。因我在灵性方面的小小贡献,我写的所有东西都是关于宇宙音乐的。当我读到你写的作品时,从科学的观点来说,我会想:“哦,天哪,科学和灵性真正融合在一起了!”

那是真的,顺便说一下,下个月我们将首次在瑞士的日内瓦(Geneva)推出目前科学界所能制造出的最大的机器——“大型强子碰撞型加速装置”(Large Hadron Collider)。它有17英里长。我们希望真正创造出一个无法看到的、更高的音乐粒子,因为我们是八度音阶中的最低音。你在我们周围看到的一切,都是在小橡皮筋上以最低频率发生的振动。我们计划在“大型强子碰撞型加速装置”上生成更高的频率。
当然,目前媒体已经扰乱了整个计划。媒体说,这也许会创造出能够吞噬地球的黑洞,那是非常愚蠢的无稽之谈。我们不可能在瑞士的日内瓦制造黑洞。我们希望获得更高音阶的音乐。那就是和这台机器有关的一切。

□这与球体音乐有何联系?

几个世纪前的音乐家尝试用球体音乐来解释星球的运动和我们看到的周围物体的运动,他们想用音乐来解释运动。
这一想法没有在科学上实现,因为伊萨克•牛顿(Isaac Newton)来了。他给我们带来了运动定律,这一定律对我们的卫星和星球都很有效。之后爱因斯坦来了,他说牛顿走得不够远,星球的运动不仅能用重力来解释,也可以用时间和空间(即空间和时间的曲度)来解释。现在,我们意识到,爱因斯坦也走得不够远。我们不只要走到时间和空间里,还要走到多维空间,穿过创造音乐的震动弦——多维空间震动的音乐,把我们带回到球体音乐。从某种程度来说,我们已经完成了一个哲学意义上的完整的循环。

□让我试着跟上你的思想。根据你的橡皮筋暗喻和振动改变现实的概念,难道是说,假如这个星球上有足够多的人提高他们思想的频率,那么我们就可以改变星球上的物质本质?

在这种情况下,我不能完全肯定如何改变这些粒子的频率——当然有一种方法,即通过大型强子碰撞型加速装置来实现。

□当然,这就是音乐,是吗?

是的,另一种可能是,使用我们的绕地卫星来完成。顺便提一下,我刚才忘了说:我们现在有一张宇宙初期的照片……宇宙大爆炸初期的照片。人们经常抱怨我们物理学家,我们没有图片证据证明137亿年前发生过爆炸。但是现在,我们有了图片:爆炸自身的图片。这些图片是在微波的环境中拍摄的。如果你去美国宇航局(National Aeronautics and Space Administration,以下简称为NASA)的官方网站www.nasa.gov上,输入WMAP,就可以看到有关大爆炸的图片了。这的确是一张令人满意的基础照片。那里有“创世纪”,有起源,也有宇宙的突然爆炸。我们现在拍到了这一场景。那么下一个问题是:“大爆炸之前发生了什么?”
我们认为,那就是超弦理论出现的地方。
大爆炸是在137亿年前发生的。实际上,超弦理论将你带到爆炸之前,因为在爆炸之前,时间还没有开始。超弦理论预言,那里可能存在其他宇宙——因此,如果多元宇宙像一次泡泡浴,那么我们的肥皂泡会从另一个肥皂泡中分离出来。就好像你在洗泡泡浴的时候,肥皂泡一分为二或者两个泡泡猛力撞击,融合成了一个泡泡。
那就叫做超弦理论。
一些物理学家,特别是我在普林斯顿(Princeton)的一些朋友,大力推崇大啪理论(Big Splat Theory);然而,我的一个在MIT的朋友推崇成长理论,也就是“膨胀理论”。这一理论认为,我们的宇宙是从另一个宇宙中发育而来的。我们不确信哪一个理论是正确的。但关键是,我强调,宇宙和时间并不是从大爆炸开始的。我们认为,我们可以来到大爆炸之前,我们将在2014年左右测量出这一时间。届时,我们将发射新一代的卫星,帮助我们搜集图片,不只是大爆炸的图片,还有形成那一瞬间的图片,那一瞬间我们的宇宙正从子宫中分离出来。
也许,我们甚至将发现脐带——联系婴儿宇宙和母体宇宙的脐带。目前我们还未实施这个方案。然而,我们早有安排。现在我们没有证据,但是到2014年,当我们发射新一代卫星去寻找联系我们的婴儿宇宙和母体宇宙的脐带时,一切都将振奋人心。

□这的确是振奋人心的,特别是因为它与一些更为深远的概念联系到一起。这些概念来自玄学界。我的几本书也讨论了太阳的星际脐带。请你谈一谈宇宙的脐带。这很吸引人。

平行宇宙的观念是出自宗教和精神领域的另一主题:存在的其他平面。例如,天主教会一直相信天堂和地狱代表两个不同的精神层面,且与我们的宇宙并存。我们物理学家暗地里嘲笑这一观点,但是我们不嘲笑任何人,因为一切证据证明,确实存在其他宇宙,其他平行宇宙。
例如,想一想你客厅中的收音机。你的收音机收到某一个频率,但你要知道你的客厅中还有几百个频率——有莫斯科广播(Radio Moscow)、哈瓦那广播(Radio Havana)、BBC……很多你不能听到的频率。现在提出疑问,你为什么听不到呢?这是因为你的广播与其他波段不一致(我们说不再匹配)。那是科学术语:它与BBC隔离了。你收听美国的摇滚电台,而听不到来自伦敦的BBC;你与BBC电台隔离了。
目前,我们认为,周围的一切都在一个特定的频率上振动;有在房间里振动的短波,也有来自其他频率的波段。每个频率都代表一个不同的宇宙;比如,在你的客厅中也许有恐龙的波动函数,还有一个埃尔维斯•普雷斯利(Elvis Presley)依然活着的世界的波段函数。
这些是我们从未接触过的频率,我们与它们分隔了;这一理论被称为“多世界”(Many Worlds),它是由一位名为休•艾弗雷特(Hugh Everett)的物理学家于20世纪50年代在普林斯顿提出的。
这个理论第一次提出来的时候,遭到人们的嘲笑,但是现在,我们开始相信艾弗雷特是对的,相信多世界真的存在。我们只不过与它们分离了;我们不再与它们联系了……但是我们和它们是共同存在的。

□加来博士,你认为有人会从其他的维度中找到这些频率吗?

我道听途说了类似的一些故事。我不知道可信与否,因为我需要检测这些人是否具有好的音感,但是如果你去看物理著作,会发现我们物理学家再也不会嘲笑休•艾弗雷特的观点了。“多世界”理论也是目前理论物理学界的首要主题之一。你可以去世界上任何一个物理图书馆寻找“多世界”理论,在那里,你会发现有500篇论文是关于量子力学的,它是“多世界”理论的量子理论基础。
爱因斯坦并不喜欢这一理论,在这个问题上,爱因斯坦是错误的。爱因斯坦不相信量子理论,但是,我们现在认为,量子理论确实描绘了这个世界。那就是我们为什么会研发出激光束,晶体管。没有量子理论,就没有广播,没有电视机,也没有激光束和卫星。所有的这一切都依赖于量子理论,量子理论的扩展就是“多世界”。

□我读过你的一本书,在书中,你讨论了“穿越维度的生命船”。你可以具体解释一下吗?我觉得它非常迷人。

很多人都跟我说:“教授,这一切都很好。你谈论了更高的维度,十维度,但是我怎样才能访问其他世界呢?我怎么到这些地方呢?”
那很难。
首先,宗教和教会的人已经谈论过冥想和宇宙穿越。而我们物理学家正在实践。我们会制造一个机器,按一下按钮,它就会带我们走,而不是通过吃一些能产生幻觉的药物或是通过冥想。在我的《平行宇宙》(Parallel Worlds)一书中,我甚至描绘了这个机器会是什么样的。
你必须理解爱因斯坦说的话:“空间和时间像一匹布,而时间像一条河。”当这条河漫步穿过宇宙时,它可以加速,也可以减速。
我们一直用卫星GPS系统获取这些测量数据,比如你的租赁车就安装了它。在外层空间,这一系统依靠加速或减速的时间。但是有一个新的难题,星球之河可能有新的涡流,分成了两条河。在这种情况下,我们无法排除有穿越宇宙的生命船的可能性。我们不能排除有带我们穿越时间之河的时光机器的可能性。
当然,现在这些都被认为是“不切实际的”,然而爱因斯坦自己也意识到,在他的方程式中,时间旅行是可能的。1949年,第一个时间旅行的方案在爱因斯坦的方程式中发现。自那时起,我们物理学家已经发现了几百种时间旅行的方案,它们能让我们在多个宇宙中穿梭。斯蒂芬•霍金(Stephen Hawking)甚至称它们是“婴儿宇宙”——让我们时光倒流回到过去或连接不同宇宙的小宇宙。
老实说,能在几个维度间穿越的机器一定是庞大的。我们正在谈论的机器是非常巨大的,因此它只能放在外太空,但我认为,事实上我们考虑的这些东西是非常令人惊奇的。通常,科幻作家和幻想作家会提到时光河中的漩涡:其他宇宙的入口。这就是交接区。我们目前正非常严肃地对待它,甚至描绘出了这种机器的蓝本。

□你希望科学何时能够实现这一重大的成就?

这不仅仅取决于我们,从某种意义上来说,我们谈论的是巨大的能量……但是有一天,如果有人敲你的门,声称是你的曾曾曾曾曾曾孙女,你不要砰地一声把门关上!这是真的,当时光机器出现的时候,你的曾曾曾曾曾曾孙女生活在另一时空,而且决定拜访她的祖先,如果有人声称是你的还未出现的后代,请不要把门关上。

□毫无疑问,斯蒂芬•斯皮尔伯格(Steven Spielberg)肯定会喜欢这一场景。

事实上,我的一个朋友,加州理工学院(Caltech)的一位物理学家,正在为斯皮尔伯格的下一部电影做顾问,这部电影就是关于时光旅行的。
斯皮尔伯格很严肃地对待这个问题。他和物理学家一起探讨,共同创造他的下部电影,就像卡尔•萨根(Carl Sagan)制作他的电影《接触》(Contact)时向一位物理学家咨询一样。他的确与朱迪•福斯特(Jodie Foster)一道,用最先进的物理学知识创造了这部电影,电影讲的是一个与外星球文明接触的故事。

□加来博士,你何时去拍一部电影呢?

近来还没这个打算。我自己都很惊讶,自己的书竟然能上《纽约时报》(New York Times)的畅销书排行榜!我真的很震惊!

□那真的很棒,我要列举你的一些重要著作:《多维空间》(Hyperspace)——不得不读的一本书;《平行宇宙》——另一本绝妙的书;而现在我们也打算拜读这本新书——《不可思议的物理》(Physics of the Impossible)。但是在我们读这本书之前,我想去瑞士看看那儿将发生什么事。你能告诉我们是具体哪一天吗?

下个月,在瑞士的日内瓦,科学家将启动一台17英里长的机器。它不具破坏性,放在地下,所以没有任何辐射会泄露出来,它也不可能制造出一个能吞噬地球的黑洞。我认为媒体的报道在这一方面出现了偏差。
2008年5月,我们将启动那个机器。我们获得了某种电磁波,只有当它达到一定速度时,才会产生这种电磁波。这种电磁波将会重新创造一个宇宙,像小熊维尼一样极小的一个宇宙——大自然母亲将会创造能量,不会有任何危险发生。
地球在这些宇宙射线中洗了一个澡,这些射线比我们弱小的人类在地球上创造的任何东西都要强大,但是我们创造了这台机器,它可以为我们打开一扇“创世纪”的动力学上的窗户。我为之振奋,因为它可能会帮助我们证明超弦理论。
有人说,超弦理论无法证明,因为必须在实验室中创造一个微型宇宙。当然,我们不是神,无法在实验室中创造一个微型宇宙。但是你可以做的最好的事情就是,创造音乐的高级形式。我们希望创造“超对性粒子”。“超对性粒子”是超级粒子,它们从五维空间中振动的小橡皮绳上发射出来,而我们处于这根绳上的八度音阶中的底层。但是那里有更高的音符——我们还没有看到它们,那就是我们想利用瑞士日内瓦的欧洲核子研究委员会(CERN)的机器来创造更高振动的原因。
如果目的达到了,它将冲击整个物理学界。它将重击那些嘲笑者。后者认为,由于我们无法在实验室中创建微型宇宙,超弦理论就无法论证。我们可以把大多数事情做好,比如创造超对性粒子,我们认为这将是振动弦上的振动的下一发展阶段。

□如果在著名的2012转变前,将这一具有重大意义的努力付诸实践,将令人激动,也非常有趣。很多人表现出他们对2012的恐惧。我想请教你:我们将何去何从,你有何感受或看法呢?

让我来说一些与此相关的事吧。我看过一些玛雅人(Maya)的翻译著作。在玛雅人的历法中,他们讨论周期,当然,这些周期长达几千年。我们正驶向周期的尽头。这里有两种看问题的方法。第一种当然是我们处于周期的尽头,世界的尽头。但是我认为,玛雅人看问题的方式却是第二种,他们认为这是一个开端。它是循环,是重生,因此它并不意味着万物的结束,只是代表着重生和再循环。我们过去已经经历了几个周期,而且并没有发生什么,我们将来也会经历更多的周期。我认为这是复兴,而非毁灭。

□我也有同感。我把它看成我们个体再生过程的一个难以置信的原型。

是的,如果是这样,我们就不再需要用悲观者的幽暗的眼睛来看待它了,我认为我们应该把宇宙看成是光辉灿烂的。宇宙的存在绝对是令人惊奇的。宇宙是一个辉煌、奇妙的地方,我们就住在这里,很难相信,一个循环将在一瞬间结束一切,而大多数循环代表着重生。那就是我对世界未来报以乐观态度的原因。当然,目前世界面临着诸多挑战,但是我相信人类有着不屈不挠的精神,我相信我们能克服一切困难。我们已经这样做了,我相信我们今后也会这样做。

□听到你这么肯定,我非常高兴。不管现在我们的星球上有多少黑暗和困难,我们需要积极的思想和庆祝的声音。我完全同意,人类的精神是不可战胜的,当我们面临最艰难的挑战时,我们也有很大的机会去战胜它们。

从循环的角度看待世界的一个好处是,你会看到过去的周期,你会开始思考:“我们曾在哪里?过去我们战胜了什么困难?”而那是未来的一个向导。我们不看阴暗和死亡,而看那些我们可以重新审视我们曾待过的地方。除非你理解了我们一直处在什么位置,否则我们无法理解自己将前往何处,这是一个事实,我们通过循环看待自己的历史,这将迫使我们重新审视历史,我认为,这会给我们带来希望,我们有能力铸造未来。

□你认为在我们身上将会发生什么呢?你如何预言近在眼前的转变?要知道,在我们前方,我们面临黑暗事物的挑战,不需要一一列举,而相对地,我们也拥有很多积极的、具有革命意义的变化。你认为未来几年这个星球会发生什么?

让我来告诉你下个世纪将要发生的事情,因为我坚信诞生与重生。物理学家兢兢业业地寻觅宇宙中的智慧生物,我们也必须严肃地分析这个问题:“我们一旦与这些外星生物接触上了,我们如何定义我们的文明?”对于我们来说,这不是一个学术问题:我们有望远镜、卫星。总有一天,我们会和他们有所交流。
一些物理学家已经提出了一种理论,认为文明有三种形态:第一形态、第二形态和第三形态。
第一种是真正的星球文明:他们有一种星球文化;他们应该能够控制天气,比如飓风和地震;他们在海洋上建造城市。100年后,一种与此描述相应的星球文化将出现在我们面前。
第二种文明形态现已在多颗行星上发展起来了,他们控制了行星……一些邻近的行星。他们将一些邻近的太阳系行星作为殖民地,并控制了整个星球的能源输出量。
然后是第三种:这种文明形态是银河的,他们真正控制了能量,不是一个星球的能量,也不是100个的,而是行星系统中几十亿个行星的能量。那就是第三形态,如果有天外文明来造访我们,那大概就是第二文明形态或第三形态。
现在,回过头来看我们自己,我们处在哪个阶段呢?我们处在零文明形态。

□好吧,那么你是如何定义零文明形态的?这一文明形态听起来好像是一种没有进化的文明。

好的。让我们来谈谈零文明形态。这种文明形态从死去的植物身上获取我们需要的能量。丛林中的野蛮人、宗教主义的原始状态、无知、贫穷和疾病仍然存在。他们只能梦想造就一种星球文明社会,但是如果你用一个计算器做些计算,得出的结果是:我们要建立一种星球文明社会还要100年,每当我读报纸的时候,我总看到第一文明形态诞生时的痛苦。
比如,什么是因特网?因特网是第一形态电话系统的开始。历史上,我们第一次看到一张星级电子通讯系统的略图。因特网是第一形态电话系统的婴儿、萌芽和种子。他们将会说什么语言呢?他们很可能会说英语。英语已经是精英语言。我在每一次会议上都用英语发表演讲,而精英商人、政客和科学家也都说英语。从现在起的100年之内,英语也将成为中层阶级的语言。中层阶级也都会说英语,就像说他们自己的语言。
欧盟是什么?欧盟是第一形态经济的雏形——我们为什么要有欧盟?为了与北美自由贸易协定抗衡,也就是与美国抗衡。因而我们看到了第一形态经济的雏形,我每到一个地方,包括我听广播、看电视,我都能看到第一形态文化的开端。年轻人喜欢摇滚音乐。他们喜欢RAP(边说边唱),并跟着最流行的拍子跳舞。精英喜欢更高级的时尚,我认为在全世界范围内,一贯如此。我们都喜欢电影,那也给了我们一个第一形态的文化形式。
每到一处,我们都能看到第一形态文化的开始,但是也有反冲,这就是我所担心的。有些力量不想拥有第一形态的星球文明,因为这一文明是多元文化的,是启蒙的,是进步的;它相信平等和未来的进步。这些力量就是恐怖分子。
恐怖分子信任负一形态的文明—— 一种由教条、严厉的宗教信条和一个非常严格的等级制度控制的文明,在那样的文明中,男性高高在上,女性被踩在脚底。
那就是恐怖分子,他们不相信进步的第一形态的星球文明。现在还不知道我们能否从零形态转化到第一形态。一些人认为,也许在2012年,我们会转化到第一形态,但是我认为2012年只不过是复兴,一个新的开始。我们将驶向何方?人们一直讨论的宝瓶座时代在哪里?对于我来说,那就是第一形态文明——星球的、包容的、进步的、科学的、民主的——我认为,那就是我们前进的方向,除非我们先把自己炸死。

□我想在你的星球级别的区分中加上一些想法,不只有所谓的“恐怖分子”不想让我们这个物种进化。在恐怖分子的身后,还有另外一些力量正在发挥作用,你认为呢?

哦,是的,我赞同。

□我明白,所以我们不必扮演任何人。

是的,当然,我们有核武器、人口增长、生化武器、温室效应和全球变暖这一系列的问题。有一些力量不想让我们进化到星球文明,因此我认为有一个抗拒时间的种族。那就是我看到的一个模式。
一方面,我们有启蒙和教育的力量,这些力量正成长为卓越的、多元文化的、星球的文明;另一方面,我们有无知、宗派意识、原教旨主义以及种族主义的力量。我们自身有黑暗的一面,这一面来自原始森林。我们仍然有自己古老的大脑,不得不在沼泽地和森林中对抗所有人,我们身上仍然有这些不好的东西。我们不知道谁会赢,但是我认为,总体上,我们会生存下来,我们将平安度过2012,最终我们的文明将走向第一形态。我很乐观。

□我也是!话说回来,请你告诉我们,你的《不可思议的物理》一书的总体意义和目的。

刚刚去世的阿瑟•克拉克(Arthur C. Clarke)曾做过一个非常有名的论断,我引用一下:“任何足够先进的技术都是无法与魔术区分开的。”
现在,想一想魔术。
魔术能够消失、隐形,又在另一个地方出现。魔术是一种将一个物体变为另一个物体的能力,它使它们消失,又使它们在某个地方出现。未来我们将拥有这种能力,比如,隐形术。两年前,在杜克大学(Duke University)和伦敦的帝国理工学院(Imperial College),我们在隐形能力研究方面获得了重大突破。我们能够使一个物体消失不见,至少在你使用微波射线的时候才能看到它。就在几个月前,在加利福尼亚理工学院,他们展示了同样有此功能的可见光。我认为,在未来的几十年中,我们将能够使物体隐形——所以,哈利波特(Harry Potter),你要小心了!

□那不就是著名的“费城实验”的本质吗?

从某种意义上来说,是的。我们现在有能力展示隐藏一个物体的可见光了,这种光还可以在另一头将物体重现。我们以前都认为这些是不可能实现的。我在大学里给学生上光学课,我常常告诉他们隐形能力是可能的,因为光和水不一样,光可以缠绕一块大圆石,然后在另一头使物体重现,就像一条河一样。

□你是怎么做到的?

有一种新的物质叫做“超物质”(metamaterial)。它上面有很多杂质,以前我们从来没有想过这些杂质,但是这些杂质可以与光线反冲,这样它就能隐藏一个物体了。
把哈利波特带过来,然后把他放进一个圆柱体中。触碰到圆柱体的光线包围着圆柱体,然后在另一头重新成形。这一原理已经在两年前被证实了。这震撼了整个物理学界。在此问题上,每一本物理教科书都是错误的。

□但这是多么奇妙啊!这就是“不可思议的物理”,是吧?

是的,这就是不可思议的物理。现在,另一个“不可思议”的东西,像魔术师一样消失,然后在另外一个地方重新出现。我们物理学家称它为“心灵感应”。在原子层面上,我们在实验室里已经能够做到这一点了。我们已经能使一个光子消失,然后让它在100英里之外重新出现,比如从加那利群岛(Canary Island,又名金丝雀岛)到另一加那利群岛。
下面,我们希望做一些有关航天飞机的事情。我们想要在地球上取一粒光子,使它消失,然后让它出现在航天飞机上。2020年之后,我们计划在月球上做这个实验。
这种实验是没有止境的。我们确实能够使一个物体在月球上重现。现在,对于原子来说,不只是光子,我们用铯原子和铍原子做的实验已经取得成功,当然,这些都是在实验室里试验成功的。我估计,在未来10年时间里,我们应该可以让一个分子,例如DNA消失,然后让它出现在别的地方,也许还可能是一个病毒或一个细胞。
在人类身上做隐形实验也不是不可能的。老实说,一个人有着50万亿个细胞。我们无法传输所有的细胞,但是我们的技术已经达到这个水平了,我认为应该考虑一下哲学上的限制。当你被传输的时候,你事实上已经死了,你消失了。你在这一过程当中分解,然后你在别的地方重现。这个场景常常在电影中出现,比如《越空行者》(Jumper)和《星际迷航》(Star Trek)。你刚刚看到寇克舰长(Captain Kirk)在一个房间中死去,又看到冒充他的一个人——寇克舰长出现在那里,说:“我是真的寇克舰长。我有他的记忆、基因、神经系统以及他的怪癖。”
这会让你怀疑灵魂。如果你看到他分解,而他带着自己所有完整的记忆重现,他的灵魂发生了什么变化呢?

□那也是一个我和其他玄学家们所担心的问题。

我们物理学家也觉得这个问题非常棘手。对你来说,自己身上的所有原子分解意味着什么?你已经不在这儿了,无论你曾经是谁……你已经走了吗?站在那儿的另一个人是谁,长得如此像你,拥有和你一样的思维模式、一样的记忆,而且他声称他就是你?在未来几十年中,这将是一个学术性的问题,我们该如何处理?有灵魂吗?我们在实验室中将要面临这样一个问题。
在《不可思议的物理》一书中,我描述了很多你在电影中看到的技术(比如《哈利波特》[Harry Potter]、《终结者》[The Terminator]、《外星人》[E.T.],《星球大战》[Star Wars]以及很多好莱坞大片),这些其实都是建立在我们物理学家认为将来会实现的技术上的。关于我们何时才能拥有这些技术,我无法给你一个时间框架,几十年,几个世纪,几年,或是一千年。

□这项技术的实现会比想象中更快些吗?让我们再回顾一下《星际迷航》,再看看那些掌上通信工具。我的一个同事说,诺基亚(Nokia)是按照《星际迷航》中的模型生产的。

是的,我们也有这样的感觉。

□我们已经有这些产品了,而且它们看起来完全一样,功能也一模一样。

是的,再看看心灵感应。你知道,心灵感应曾经被认为是占卜者和拉斯维加斯(Las Vegas)魔术师的无聊伎俩,但是我们物理学家实际上已经能够复制心灵感应的有限的几种模式了。比如,在布朗大学(Brown University),他们在完全瘫痪的中风患者的大脑中放上一块芯片,芯片大约有一美分的1/4大,然后将芯片与一台手提电脑连接起来。我们在3小时内训练他们阅读电子邮件、打字、猜字谜、打电子游戏、上网。

□那听起来有点毛骨悚然,加来博士……但是你看,想一想所有的植物人,陷入全身瘫痪,他们无法做任何事……而现在他们可以写电子邮件了。

是的,但是容我插句话,那么健康的人接入一个电脑会怎样!
将来有一天,人们会有选择的。
他们会有选择。一些人也许真的想插入一根微型植入管,他们只需要想一想就可以上网。这些都是可能的。他们会有选择吗?我们希望他们会想得更多一些,想一想插入芯片的意义,插入芯片意味着什么。
现在,如果你想知道一些毛骨悚然的事情,这里还真有一些。
我们可以利用核磁共振成像仪和脑部扫描,查看一个说谎者的脑部模式。当你说谎的时候,需要更多的能量。为了说谎,你必须要知道真相,还要制造一个借口。这些都是大量的能量,在一张脑部扫描X光片中很容易看到这些。将来,我们也许能通过读取大脑扫描的X光片结果,就可以看到思想和感情的轮廓。当然,这可能需要上法庭。
有一家保险公司拒付一个保险索赔,原因是公司认为那个人自己烧毁了房子。保险公司说:“你自己烧毁了房子。因此,我们不会陪你一分钱。”那人非常气愤,回答说:“我要去告你们,将我的大脑扫描X光片呈给法庭,我要证明自己并没有烧房子。”
将来,这些手段也许都会走向法庭。我们也许能够通过读取嫌疑犯的大脑扫描X光片,判定其是否有罪。

□你认为这将把我们带入第一种形态的星球文明吗?

一旦用激光扫描这个屋子里的人,他们的灵魂将发生什么变化呢?伦理学上的问题随之被提出来。如果你读取了一个人的大脑,那么隐私呢?我们有没有隐私呢?我们希望像科幻小说中那样剖析自己的思想吗?这些问题我们都要面对。无论我们喜欢与否,有些事情总是要来的。这本《不可思议的物理》告诉你这条路上将要有什么来临……无论你是否喜欢。
我也讨论了未来几十年星际飞船出现的可能性。
NASA已经开始考虑在将飞船发送到那些行星上去时,顺便携带些什么。事实上,我是NASA的顾问。我必须考察一些向NASA提出的建造星际飞船的提议,实施起来当然还需要几十年,不会是马上,但是我们正在考虑与外太空的外星文明建立联系。
这不再是电影中的东西了;我们科学家正在认真研究之中。微软的亿万富翁保罗•艾伦(Paul Allen)已经投资了2600万美元建造一个新的望远镜(电子望远镜)来窃听外星文明之间的对话,因此这是一项非常大的买卖。亿万富翁们开始把钱投在这一领域中,一些人声称,在25年时间里,我们也许可以与另一种有智慧的文明进行第一次接触。

□我认为已经不远了,也许在未来的某个时间,在你还记得这次对话时,但是我认为可能比25年还早些。

可能吧……很有可能马上发生。

□我真的很想问你,你想看到一个什么样的“不可思议”的发明被创造出来?你认为,人类最需要什么?

啊!我们现在最需要什么?我想我们需要解决这些古老冲突的途径,这些从人类出现就已经存在的冲突,我们还需要一些公平的途径。
当然大多数人宁愿要更小的东西:更多的钱用于医疗保险;少投一点钱给……在政治领域发生的欺诈现象。我希望我们能做一个大一点的馅饼。我们不是为小馅饼和陷入内战和进行侵略而奋斗,我希望科学和技术可以给我们一个更大的馅饼,这样我们不必常常为了分馅饼而陷入宗派斗争。
但如果要我现在考虑一个能真正解放我们的机器,那也许就是建造一个时光机器来观察未来。我们可能看到事情是如何发生的,从那里学到教训,也许有一天会改变自己的未来。

□发现这些,会不会给我们带来更多的快乐呢?

是的,但也会带来苦恼和痛苦。这是一种交易。

□好,这里我不禁想到:在你不可思议的人生和头脑中,正寻找着创造不可能的途径,也许将来出现在我们面前的一种机器就是源于你在瑞士的发明,在瑞士你建立了更高的领域。我也相信,一切都是音乐,希望有一天我们能通过外太空和外宇宙,看到在这个星球上更高的八度音阶。

是的,我也希望如此;我希望我可以实现爱因斯坦的梦想,读懂上帝的想法。我认为目前我们大家都向着一些宇宙问题前进,我们可以在自己的后院中解决这些问题吗?我们如果能活着看到这些问题因技术突破而得到回答,将是多么美妙的一件事啊。
超弦理论很有希望能把我们带上这条道路。



1<词>,2[句],3/段\,4{节},5(章)。
2016-2-3 19:06
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 11 楼』:  10 LOLI地狱 尼古拉的遗嘱 11楼

10 LOLI地狱     11楼

之前(几个月之前了吧)在yinyfly兄那看到了一篇关于炼金术的文,我就告诉他《尼古拉的遗嘱》很有意思,没想到他竟然误以为这是本小说,既然你诚心诚意的问了,我就大发慈悲的告诉你,为了防止世界被破坏,为了维护世界和平……飘远了……

《尼古拉的遗嘱》主要讲述了伟大炼金术师尼古拉勒梅(法文Nicolas Flamel,英文翻译Nicholas Flammel或Nicholas Flamel,中文翻译就更多勒)充满传奇的一生,以及与其相关的神秘的炼金术。所以这本书并非是杜撰出来的小说,而是徘徊在史实与传说之间的历史文献,因为其中含有大量的精美插图,所以应该没有电子书了!

←这就是生于1330年,原法国代书人大街的抄书员,伟大的慈善家,伟大的炼金术师,贤者之石的始造者尼古拉勒梅先生!他的一夜暴富,在圣婴公墓留下的神秘图案,空空如也的棺木,两百年后的自传都将的这位看似弱小的老头紧紧的与炼金术连在了一起!
←直接导致尼古拉先生走上炼金之路的《犹太人亚伯拉罕之书》上记载的传承炼金术的七幅图之六。虽然我们看到这些蕴含着世界的真实的图就像读《相对论》一样茫然,但也不难看出炼金术与《钢之炼金术师》里的有着本质的不同。
←眼熟吧!惊讶吧!下巴掉在键盘上了吧!没错,这个图案看起来很想世界卫生组织的LOGO对不对!谢谢各位送了我一栋房子,虽然你们只是一人送了一块砖……好吧,我知道你想到了荒川弘设计的那个LOGO,这个同样是《犹太人亚伯拉罕之书》上记载的传承炼金术的七幅图之一。
←       最早传入欧洲的炼金术文献之一的       →
《翠玉录》短短的13句话揭开了炼金术登上历史舞台的序幕。左边是20世纪初由一些拉丁文研究者根据一份12世纪的拉丁文手稿译出的,而左边是牛在1680年译的。什么?哪头牛?就是牛123的牛。还是不知道?就是Issac Newton啦!但牛顿的那份貌似是古英文,看不懂呢!
之前提到的尼古拉先生在巴黎圣丹尼大街的圣婴公墓第→四存尸房的墙上留下的神秘的图案。1612年《解读尼古拉勒梅刻在巴黎圣婴公墓第4墓室拱墙上的难以理解的符号》在巴黎首次出版,作者署名尼古拉勒梅,如果看到这里还没有什么疑异的话请拉到第一幅图再看看。冗长的名字显然是编者加注的,而作者用第一人称讲述了自己寻访贤者之石,探究真理的艰辛过程,同时解读了那神秘的壁画,揭露贤者之石制造工艺的壁画。
←这是现藏于麻省理工学院的1693年牛译《解读尼古拉勒梅刻在巴黎圣婴公墓第4墓室拱墙上的难以理解的符号》手稿,什么又问牛是什么?自己拉回去看啦!牛顿热衷于炼金术并不仅仅限于他“无所作为”的后期,有人认为牛顿早已从《解读尼古拉勒梅刻在巴黎圣婴公墓第4墓室拱墙上的难以理解的符号》中获得了灵感进而得出了重力和光的理论!所以牛顿被称为“牛”!不对,是“第一位科学家,最后一位魔法师”!
黄道十二宫在炼金术中有独立的意义。《尼古拉的遗嘱》中还→有很多此类的图,例如yinyfly兄那篇文章里的那幅出自1772年丹尼莫里耶《尼古拉勒梅的炼金术》中的拟人化的七大金属。
←   尼古拉先生的故居。左为1990   →
年,右为2004年。现在已经是“尼古拉勒梅餐馆”了。记得《钢之炼金术师》里贤者之石也是记录在菜谱上的吧!
←伟大的面孔。炼金术师图谱。除了那个→很牛的牛,还有很多在其他领域也有很高造诣的人物,例如化学家波义耳也被列入其中了!什么?你问我右边那幅图右下角那是谁?既然你诚心诚意的问了,我就大发慈悲告诉你,为了防止世界被破坏,为了维护世界和平……前后照应嘛……OTZ
我要说的就这么多!



1<词>,2[句],3/段\,4{节},5(章)。
2016-2-3 19:26
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 12 楼』:  11《关于时间》 12楼

11《关于时间》     12楼

《关于时间》
         
根据央10最近推出的四集探索宇宙大爆炸、宇宙密码科教片理论,宇宙中若干的星系在形成之前都集中在比原子核还小的空间里.那么,这包括太阳系,包括负载太阳的银河系.那么,所谓时间,在此之前已是一个无法比拟的废话.即在星系特别是太阳系形成之前不可能说时间的.因为,如今地球上的一切时间,包括生活的,工业的,科研的,都是依照地球自旋和公转而定格的.
没有地球自旋和公转就没有我们理解的时间语言。没有我们语言所指的实际运动事实,也就没有时间事实。
     
所以,时间从根本上说就是太阳系内地球运动的记录.地球匀速运动过程的记录.从一个运动点,到另一个运动点的记录.就分段描绘成时间.即把地球绕一圈分成24格就成了一天的时间,一格叫做一小时,一天24小时,一小时再细分为60格,为60分.一分再分60格,为60秒.生活上,到分就可以了,运动比赛上要到秒,而科研科技上更细分毫秒,微秒……
     
所以,时间只是当今地球绕太阳匀速运动过程的一个描绘.离开了这个运动,现在使用的时间就不存在了,而,现今对星球天体过去未来的时间计算,都是鉴于地球运动的照搬假设.并不代表星球,天体过去未来的事实.过去和未来物质天体的运动与现在根本不是一回事儿,在那非匀速的四维空间,你怎么去对等?也就是说宇宙大爆炸以来120亿年了,这只是根据现在的数学模型演译.当说,宇宙在膨胀,星系在远去,在过多少亿年……..等等,这样的语言都是依照今天的常话所假设.都是依照数学模型所认定,所推论.
     
那么,余此到过去未来那个运动事实如何呢?我们今天地球上的人没有理由推翻,也没有权力描述.如今的种种描述也许可能就像哥白尼的地球中心学说被后来哈勃推翻那样,被亿年后的事实推翻..
     所以,时间,时间,时间.实际是宇宙时段中地球这个天体运动一个很狭小的概论.它只存在当地球太阳这样一个运动关系的狭小过程.
    因此时间,只表达地球与太阳运动的关系,特别是现今文明社会时代的关系.比如,在32亿年前,三叶虫时代,后来的恐龙时代地球都没发生时间语言事实.
   
所以,时间,是文明社会人类对地球与太阳运动关系的一种理解而理论成立.可是,对此五年前,2003_____2005年时,在央视网站科技论坛上,曾有一大批网友为时间与运动的关系发表个若干讨论.本人也参加了这个讨论.为此,集结了一些文章共十四篇,为了使喜欢本思维.永恒网页并且对什么是时间感兴趣的朋友略知一二,故而今天把这些文集结按发表时顺序排列.转余此.
     是谬是实孰尔评判.
                                                       思维.永恒    2008`3`24

  ,
   一,时间你躲在哪里?
   
时间,你可知道,我们目前的央视网论坛科技频道正在展开,时间辩论大战,无数只眼睛在盯着你,无数张嘴在对你说七道八议论纷纷.那场面,仿佛古战场的尘埃与硝烟腾起,还夹杂着战马嘶鸣声.........
    “时间"==???..时间的身份?时间属于哪一家的主人.......
     
时间,在科技生产中以秒计,以微秒毫微微秒计,每秒万亿次的电子计算机其时间,间隔已精确到1秒/万亿.时间在高科技中已具有绝对位置.时间在生活中已用之为常,早晨出门看时间,乘火车看时间,过年过节想时间.可以说,没有时间概念这世间的生活一切皆乱套,皆停顿.....我们珍爱时间和稀奇时间都是因为"时间"里包含了这么多物质和事情..时间在哲学家的眼中已经是一个抽象的至尊.那么,我们能走近时间么?同时间对话么?同它辩解辩解,理论理论,交流一番?
   
其实,时间从人类社会一出场,就并没有亮明它的身份.而对时间的拷问,是在人们意识到时间的无比珍贵,又有了高度抽象高度反思高度省悟能力之后.看吧,那许多关于时间的话题都勾起了人们无尽幻想,比如电影<<侏罗记公园>>,把人们引到了恐龙存在的时空里去,特别是科学发现120多亿光年以远的天体存在,更是引起了无尽遐想.于是有人幻想时光倒流,有人特别爱哼唱"让地球忘记了转动,让时光懂得倒流......"的歌.这等等都是我们的思维活动,我们的意识活动,我们对发生在自己身上和周围客观事物中的事物反省.
   
然而,这时,时间却和我们藏猫猫了,它躲在哪里不见了.于是,关于"穿越时空邃道"的争论,就争论不休.持"有时间邃道",可以穿越时间邃道观点的人.和据守无"邃道",不存在穿越的人,均愈战愈勇,近乎白热化.于是,我们曾在央10节目中看到专家出场"摆平".却原来,时间看着我们,找它找不着万分着急的样,竞躲在一个地方偷偷发笑呢.时间",你让我们不能忘记你,因时间带给我们理想与财富.时间抓得越紧骤带给的财富越多,叫我们怎么不怀念时间嘛.伟人曾说过."多少事从来急,只争朝夕".平民常说"少壮不努力,老大徒悲伤"

:通过时间,会反映出很多物质和事情出来."早晨,阳光爬上了山坳东边的山坡",早晨是时间,山坳和山坡是物质,阳光也是物质;"1949年10月1日",这个时间里在天安门前升起了五星红旗.天安门和五星红旗都是物质.时间对我们这样亲切,它仿佛就在我们身边,我们再仔细找找看啦.....门仿佛"吱呀"一声打开了,是谁打开的!!对,是他,______就是思维,人人都有的思维.哟嗬.....终于找着你了,逮住你了,你这个"坏东西"!!你这个调皮捣蛋家伙.我们找你找得这样费心费神费情的时候,你竞躲在大脑记忆库里发笑呢!........此是童话,趣事之后,我们不得不深思,时间到底怎么________
时间是人从主观出发,从主观,对客观的概括`综合`把握规律.首先,时间是主观对客观匀速运动的描绘,时间是意识的产物.时间是一种意识结果.时间并非宇宙内的物质本身.包括,对远在宇宙天际120多亿光年以外的物体描述(科学探测),也是借助光速匀速运动计算的.如果,假如,也许,光速经过银河系以外的那些大大小小星系时,光速有所"逗留",亦或"突然"快跳.那么,"这个测算就不为真".只是,在至今,这"突跳"与"逗留"的假如,不能成立的情况下,我们依然.........而,其次,时间是记忆(记忆与意识在概念上不完全重叠)对匀速运动的回顾,描述与表达.因此,时间的本质是匀速运动.时间的机会因素是记忆,并且由这记忆上升成为意识.而意识对匀速运动的回顾`描述,与开发简直千奇百种.比如,用来纪年,用来记岁数;比如制造手表;比如按排机器运动的先后次序`自动流程.包括气缸活塞与连杆之间的衍接,机械手臂的操作.试想,在所有机械化运动和自动化运动中.如果没有一个匀速可以"抓住"[被用来计算,调控,排次序],那么,就达不到机械化,自动化.运动员在短跑赛中的运动"不是匀速"的,所以他们在这中间的运动无法用时间来描述.但是有一个办法,人们采用"算总帐"的方法,从起点到终点计时.所有不是匀速的运动,运动过程中都不能用数学时间来表达.比如,一张飘飘摇摇落下来的纸,非匀速的运动无法产生一个"纸张下落公式".而著名的自由落体公式F=1/2gt平方,其中的米/秒则是由地心引力支配的匀速.在小学算术中常有从甲地到乙地,相距多少公里,汽车时速多少公里,算出用时多少的作业.要知道,这个算出来的"时间",在实际中是没有支持的.任何一个汽车(哪怕封闭的高速公路,航空也会遇气流雷雨改速)都绝对做不到真正匀速.所以凡是由人的意识支配的运动都不是绝对(真正)匀速.时英钟再多么"准确",都还得被迫与地球旋转周日,定期"核对".发电厂发电机的转速也不是恒匀速50周/秒.目前我们真正"信得"过匀速是地球旋转,它是发布匀速运动的绝对权威.如果,地球匀速改变时,也许是地球未日来临之日,至少生物钟会.........还有碳14的测定,同位素的测定,都依赖了宇宙中某一特定的匀速机制,元素的运动衰变必须是匀加速或是匀减速才行,才有可能给人提供联想的机会,最具说服力的高等数学物衰公式才能建立.
     因此:记忆+匀速==时间
   
时间不是物质.时间不是运动本身.时间是思维里的概念.时间之所以让我们看不见"全身",我们能看见时间的原体(物体运动),却看不见时间这个影子.时间是物体运动后产生的"影子".时间经人的思维抽象`概括,已经蹲在我们头脑里了,若问它在哪里?!嗬,好哇,我们看见你了,你猫在大脑记忆库里呢!?你让我们好找!!!找死个人了哟.....嗬.记忆概括了时间的起端和终端,没有起端的记忆,就没有终端的概括.留在大脑里的记忆可以把时间重叠.这个"重叠"过程就是意识加工的过程.思维的过程,抽象与综合的过程.从而完成主观对客观的认知.认知的结论,又留在记忆库里.所以记忆是"时间"涎生的桥梁,是时间的元素之一,匀速是元素之二.一些大跨度的忆,比如山体擦痕对冰川的记忆,化石对古生物的忆,陨石对天外来物的记忆.又得经过人的认知能力方能化解为时间.而这时,人头脑里则动用了知识的记忆.
   
所以,无记忆不成时间,无匀速不成时间.时间,时间呀,时间啦.我们要认识时间必须从思维的逆向之路,倒转回去才能在路上找到.在逆向路上总会有人告诉你时间的身份,告诉你时间是哪一家的人.而,始终沿着思维(意识`认知能力)的来路寻找,朝来路方向继续探望,张望,"打听".是不会有结果的.
     
     二,
    电影拷贝"时间"无效
   作者:思维.永恒3   时间:2003-09-18 16:39:00
________写此文是针对有网友坚持“时间可以倒流”,并举电影拷贝时间,现场可以看到几十年前的事实,而就此讲时间倒流,因而写此文.___2005`8`11加注
     
现在人们已公认,时间与运动(运动里必须有物质)有关系,只是还没有公认:匀速+记忆==时间.(匀速.必然是运动的匀速,物质运动的匀速.匀速不为空).如果你愿意确认,这个大家还没公认的等式,以此为基础来讨论拷贝的话,.那么,我就说,在电影倒放的拷贝里,没有匀速这一项.注意:片子本身(胶片转动)的匀速,和片子上面记载的信息匀速是两回事.这里的信息己是"照射"了物体和走动人物后的信息,是物体(从信息而言人也成了物体)运动的代码`代号`象征.己经高出了物体一个级别,物体(和物体运动)在低一级别,两个不在同一个级别(哲学叫范畴)的概念不能平等同质互换.信息(画面`伴音)在倒流,仅仅是信息倒流,是信息倒流的事实.不是原物质体运动的事实.所以也就没有"时间"倒流的事实支持.这种"信息时间"倒流的事实,在我们每一个人头脑里都轻易发生着,比如,回忆1秒钟前,十年前,甚至六十年前发生的事,自经亲手做过,亲身经历过的事,都时不时在发生.这等等"时间倒流"中的时间,不是大家正在讨论中的"时间"
    三,,
   且不鱼目混珠
   作者:思维.永恒3 时间:2003-09-26 12:10:27 ________为了我的鱼目,不去与珍珠混在一起.
______当时写这一篇的原因, 是因有网友出于恭敬对本人“时间你躲在哪里”文恭敬有加,为不冒牌站领那位置而写此文.__2005`8`11加注
    1,这不是一个时间计算公式(指匀速+记忆=时间).
    2,时间的计算公式,国际上已很完美,运行很准确,很可靠.
    3,我不善于列计算公式,也不会列计算公式,我的数学能力很差.
    4,我只是在回答意识上(关于时间)的一些问题.一些困惑,一些未解的争论.
    只针对"时间与运动的关系", "时间邃道", "时间倒流"这样一些问题.这些都是一些意识问题,而不是科技计算问题.
这个(匀速+记忆=时间)等式是个概念"加",是三个概念的等式.这种做法有悖于人们的习惯,可能正是由于人们习惯的做法才产生那些(关于时间)未解和困惑.可能只有通过悖于习惯的做法才能求出(意识上关于时间的)未解,不是么??对两个概念的加"+"号,在脑内实质是在进行综合`归纳的过程,用俗话讲成复合`重叠的过程.等号"="则巳是出结论了,结论写在"="后面.所以"+"号和"="号(只)是反映脑内的两个运动(思维)状态,这里不用"+"
"="表达普通数学意义.
   
下面,这样来描述这个等式的流水线过程:当关于匀速的事实发生了,"事实"经脑内意识(思维)第一次处理后,判断为匀速(概念),这匀速概念经脑内记忆贮存(也有不贮存的,那都是以生活理念的形式而流走了),贮存目的是为了思考(对于颁布时间和开发时间的人,这一步是绝对不能少的.舍此便无法颁布和开发.舍此,公众意义上和科技意义上的"时间"就不能诞生)和(集体)研究,用一句话表达:等式左边表示,脑内完成了对物体(物质)匀速运动事实的概念贮存,等号本身则是个过渡,它表正在思考(研究,讨论,征求意见,验证数据,作数学模型等等)之中,思考的本质还是在进行综合`归纳等,一俟"思考"结束,结论就写出来了,放在等号右边了.
      那么,列这个等式的与意义?再整理如下: a,解决哲学家都回答得吃力的"是否有时间邃道?"问题;b,匡正<<时间与运动的关系>>主题辩词的偏执与缺陷;
c,勾引起人们对时间的想往与钟情.这便是为了不引起误解,我对该等式进行的修饰和限制.以避勉和国际流行理论"鱼目混珠啦.
     
那么,保留这个(异类的,业余的,自编的)等式目的,是在于它一目了然,有利于简化对时间的理解.这个等式所蕴含的概念内容(步骤,关系,起因,结果)都很准确,(很)不可动摇.只是等式书写符号需另加标识,欢迎网友凑兴.我(对时间的)这(论证究方法)实际是在做着意识与物质交界的工作,在意识与物质的临界点上.在自然科学与社会科学的结合部.它们(临界点或结合部),曾经是一个"几不管"的东西,一个未曾开发的沼泽地.于是,在未说明之前,这位网友"置疑"的出现,便很理所当然了.也不知这样说明和解释之后,离您的想象还有多远??
    5,我对意识的纵深理解
   
其实,时间也好,运动也好,匀速也好.这些都是人的意识产物.没有人去发现一个物体正在运动,它就没有我们生活中的运动意,人发现了,才创造了(针对这个物体的)运动这个词(概念).世界上所有的知识(无论科技的文学的......)都是人的意识对物质的反射_______物质发出信息,人类接收信息,"信息"经脑内处理后,又对该物质赋予,照射在该物质上_______实质是关于该物质的什么什么......的知识
.这份知识在脑海里一出现,就立即在脑海里出现该物质的景象,引起对该物质的注意........
所有在意识的哲学和概念,认知上的争论,都是由于"没有打开意识这个核挑壳".很多事,大家都是"不被知道"地蒙在鼓里,被争论不休.
     
我们的意识,在面对现实时,也象面对地心引力一样________被"现实"这个引力"引"得失去了知觉_______失去了对现实以外的知觉.瓦解了醒悟能力.失去了(这种知觉)就等于有一个"核挑壳"在包裹着我们________的意识.
于是对于时间的(受)蒙蔽也就产生了.而,在我看来,(认识时间,揭开意识的蒙蔽,己经)是轻而易举的事(了).
他(对时间的认识)准确的意思是:有了匀速运动,再有了对这匀速运动在脑内的记忆,(即必须将这匀速运动的信息数据资料传到脑内来,并且发生记忆.即把信息在脑内"固定"下来);再在脑内发生对这已固定的信息,进行一系列思维活动.包括判断,推理,综合,归纳等活动.由此,依据(头脑内)曾经有过的关于"时间"概念的记忆(叫知识),作对比,类比,最后作出判断,作出结论________于是"时间"就在脑内诞生啦.(经过了这样的过程,我们也不怕别人说"因时间在脑内产生的"而唯心不唯心).或说,有了这样过程的,有了这样经历和这样"加工"的________"时间"就在脑内涎生啦.脑内诞生"时间"!!!"时间"一定涎生在脑内!!!只有脑内才涎生"时间"!!!不敢于承认这(三句话)一点,就不能解开"时间"那个包袱.时间是意识对运动(特别是匀速)的加工,对运动的确定,经过确定后又贮存(记忆)在脑内.现实生活中,并非完全贮存,大量的都删除了.
    6,"时间"是意识的产物
   
(仅凭)物质的运动本身,是没有时间意义的.比如说"电子绕着原子核在运动".并且电子还在自旋,有了这样的运动却没有关于电子绕行运动的时间.设有电子绕行"时间"这一事实发生.没有发生的事实就不叫客观存在.客观存在的只是电子绕行运动本身______正在发生着.[这一点,将在下次用来排斥"绝对时间"].这说明,必须要有人把意识加(加注,关注,领会)在运动中的物质(物体)上,才能(在人的脑内)出现时间.成为我们心目中的时间.那你再说,现在天上看到的那些星星,它们不在运动吗?该星星上有没有时间?[其实,我们人总是把自己排斥在客观世界之外,自已钻到那个"核挑壳"里去受蒙蔽.对于宇宙天体而言,我们头脑里意识(包括"时间"概念,包括我们的躯体在内).不也是宇宙自然吗?也是宇宙的客观,甚至脑内神经元上信息的运动,也是宇宙的客观].
     
再如,当一个钟挂在一个长期没有人去的积满灰尘与布满蜘蛛网的房间里,不管它是多么石英钟地准确.都不会从这个钟上,反射出"八点了,十点了"的意识活动.钟表大小指针的格位,只是一些符号,没有人去"读",它能变成时间??又如,当人类没出现时,从地球混沌洪荒到类人猿阶段,地球早晚,日起日落,地球的旋转与现在一样准确(而且匀速).可是那时,在地球上并没发生"时间"的意识活动事实.直到地球上有人说出了第一声"哦,太阳落山了"
,"喂,该吃早饭了".于是,地球上发生了时间事实.地球球的意识"时间"诞生了.
     于是,延伸到: 所有的物,只有人的意识去发现,才能成为事.
所有的事,都是由人的意识生成的.事物不是一个整体.有物不一定有事,有事一定有物.有(物的)运动不一定有时间,有时间一定有物.这是一个可逆(后者)和不可逆(前者)的关系.
人的所有意识都可以生成事,"时间"是一个事,不是一个物.
__________所以,"时间"是意识的产物.

    宇宙中发生了什么?
________关于时间的肯定回答
     目录:1, 宇宙中曾经发生过什么?
            2,抽象
           3,时间梦幻
           4,时间的身份
           5,理解时间困难的艰硬点在哪里
           6,尾声
_______,
      1,宇宙中曾经发生过什么?
      关于地球起因和宇宙起因, 的描述正在日趋成熟, 人们已基本接受了"宇宙由大爆炸而来,
地球40多亿年前才由炽热岩浆冷却下来"的观点.地球上开始没有生命, 也没有意识.
宇宙中迄今也没找到地球以外的生命.没有生命,没有意识.而且只有高级生命才有意识, 高级生命不过以百万年计的历史.
     在人类出现以前,地球上没有发生______由意识产生的时间事实.( 哪怕说说"早上太阳升起来了"这样的意识时间),地球在宇宙中本己不为再说,于是,
在人类没出现以前,宇宙中没有发生"由意识产生的时间事实".发生的才是事实,没发生的不是事实.发生在什么时候,就是什么时候的事实.脑内正在进行意识的时候,思维的时候,思考的时候.也有一个发生事实存在.
既然,人的肉体也是地球上物质的一部分,人的头脑也是这一部分,
内神经元的运动(产生意识),也是地球上发生的事实,那么,也是宇宙中发生的事实.神经元运动发生的事实(即意识发生事实),直到今天,如果不是脑神经解剖学告诉我们,我们谁也不知道这个发生事实(直到今天,了解和知道,理解这个发生事实的人为数还不多).
     
我相信并且理解这个发生事实.我所有关于意识产生时间的论证都是从,这一线索出发的,这一理论依据出发的,这一理论武器.迸而这一认知能力将为我们打开时间的门.(不指时间工程计算角度,仅指意识时间感知).于是,头脑中思维(神经元运动)的发生事实______意识正在发生中的事实."他"不仅是本人(头脑)的事(事实发生),也是地球上事实发生______也是宇宙事实发生.于是,在人类以前,宇宙中没有任何关于时间的事实发生!!!
______因为,那时宇宙还没有头脑这个容器, 这个工具,这个载体.
     
于是,在宇宙中,牛顿的绝对时间是1687年间在他头脑里发生的事实;霍金的<<时间简史>>是在他头脑里二十世纪发生的事实.牛顿的头脑和霍金的头脑都不是120亿年前发生的事实.霍金用他(20世纪实际发生的)神经元上信息组合的(120亿年时间)词,这只是一个抽象的词,而不是120亿年前发生本身.神经元运动是事实发生.是20世纪的事实.20世纪思维运动结果符号,代码,所象征意义不象征120亿年前发生了那种………那种什么呢?把那种…….说成“运动”词是今天的事,那时谁也没在那里发生“运动”词事实.因为,宇宙(那时,现在,将来)不会自已说话,因为人在那个熔炼阶段无法……..
连“运动”词也没诞生,哪里还有从运动上升到时间的事实发生呢?嘴里说出(和文字写出)“120亿年”词语,是20世纪实际发生的事实.120亿年前只发生了那种……...[既不能叫“运动”词,也不能叫“时间”的事实].对待任何_______我们只要说,你实际发生了什么吗?你曾经实际发生了什么事实,你正在发生什么事实.实际发生事实.这是唯一澄清点.唯一可以用来澄清时间的各种纠纷,困惑,名种未解.
     
120亿年前只发生了那种………人类出现以前只发生了“那种”…….至今地球上也许还没有被人类发现的,将来到人们发现了可以用运动和时间来描述的_______目前,它们正在那里发生着它们的那种………..在它们那里,没有人的意识发现事实,它们自己也不能发生“运动”词,或“时间”事实.于是在人类意识以前,尽管宇宙中有今天我们说的运动存在,却未曾发生今天我们的时间.它们在那时,只实际发生了那种…………混沌?_______
     2,抽象
     
人接收的所有信息都是抽象的.人接收信息有视觉和听觉.仅就抽象程度而言:听觉中(从耳朵进)得到别人的语言声音,和视觉(从眼睛进)得到的文字形象,抽象程度最高.所有从视觉(从眼睛进)得到的自然(图景)形象,和从听觉得到的非语言声音.抽象程度同阶.都是"原创",真迹,第一手资料.
   
最基本的抽象是什么?当,看一张桌子,桌子物质分子结构中的分子,一个也没跑到眼睛里来,桌子分子的元素不计其数,它们作为桌子的物质信息一个也没到眼睛里来.我们所看到的"桌子",其实是光线的变形______按照“桌子”的要求作的变形.是按照桌子要求变形后的光线进到了眼睛里来._______光线到桌子那里去“取了样”,光线完成了最原始的抽象.所以我们需要了解的桌子信息,是经过光线转达的.光线的这一转达,完成了对物体(质)的原始第一次抽象.光线的这一转达,虽不是(桌子)面目全非,却已是(桌子)本质全非.所以,我们往往要“透过现象看本质”.却要花另外的很大功夫.起转达(传递)作用的是一些光素.这些光素已不是桌子本身,只是桌子浅表信息面.再经过视网膜的光电转换效应,又完成了第二次抽象.又完成了第二次质(光---嗟?的转换,虽然光线所夹带的意义没有丢失.进到脑内后还有判断`归纳`综合等多层抽象…….(也可把多层称为"第三次"抽象.在第三次抽象中,还有某些质的转换,虽然桌子浅表信息的老祖宗意义还在)所以.当我们意识作出(对桌子和其它任何可见物体)结论时.巳经抽象了"千山万水",已经跋涉翻越了“千山万水”.成为一个(只具原物体意义,不具原物体质量重量的)抽象概念符号了.而,语言信息在进到你的耳朵时,文字信息进到你的眼睛时,更早己是经过别人的头脑(治炼过)抽象过一次的(意识信息)了.所以抽象程度最高.这样的抽象性,带来了人类在脑内作信息交换[各种信息在脑内“短距离”`“近距离”`“零距离”地交换]的可能(性),和产生逻辑推理的条件.这样抽象的结果,使没有质量没有重量的抽象符号,已经不能沿逆路(抽象符号不能--?第三次"--嗍油?-喙馑?--嗤渡渥雷?回转去推动桌子(物体)本身,______象桌子推(反射`反挡`反抵)动光线那样.
     
抽象的符号只用于(投入到)人这样的高级智慧间交流,只在意识的范畴交流.这些交流构成(人的,社会的,人类的)意识活动.这些意识指导人自已的行为,却不能直接(通过手动,己经把意识在小脑转换成指令了,"小脑里没有意识"[见某书xx页])指挥物质(物体)……运动.(那些"意念移物"者就是在玩弄这种把戏.).所以抽象的符号(意识)只在意识范畴交流,不能直接触及物质.所以!!!你在屏幕上论文里写的"运动,物质,物体"也只是抽象的意识,而不是运动`物质`物体本身,_______那"…...."号里只是三个词而已.词属性抽象.词只是意识的表象.为什么,我们议到意识与时间时还那么惧怕说"时间是意识产生的"呢?!!"时间"这两个字是通过意识说出来的,为什么不是意识产生的呢?也不是一块木头和另一块木头交流的信息.所有的语言文字交谈本身就在意识之中(摆在每一个网友屏幕面前的不是语言文字是什么?).只有意识才能产生语言文字,理解语言文字,从而实现语言文字的交流______即在意识范畴的交流.这是在高级智慧间的交流,而不是人与物的交流.在物与物之间也不存在这种交流.就人的本质而言,一个人的意识也不能直接指导他人的行为,因为一个人的大脑不能直接和另一人小脑直接连,不能直接完成转换.只有通过语言和文字.语言和文字在人体(包括头部)发出来时,虽然把(意识)信息转变了形式,并且在声波(语音)和光线(纸上文字反射光线)于空间的传递过程中,信息形式又再次发生了转变.但其抽象的意义还在,(意识内容在传真过程中).所以阅读文字和听取语声都,比直接看桌子和听水声等要抽象得多.由于文字(比自然物更抽象)的抽象性,所以,我们此刻在网上就屏幕上的文字作讨论,就比直接面对实际物要抽象得多.
    抽象给我们带来什么?抽象最伟大的意义是带来人类,从其它生物中迅速提升(升格)智慧的效果,______使人为什么是"人"而  
成为可能!!!由于愈抽象(信息愈简便集中,愈零距离),我们人类认识事物的能力愈具有普遍性广泛性和闪电性.愈具有超越凌驾于其它生物和无机质之上的能力.这便是人为什么是"人"基本素质所在.越是更充分具有"人"的意义(高智商者),其思维的境界越远离最原始的事物,时间就是最始的一件事,从黄帝尧舜开始就有纪年了,在21世纪我们大家反倒都在问:"时间是什么?"`"
时间是物质吗?"`"时间是能量吗?"……,踩在脚下的鞋我们却己经不知道它是鞋了._______这皆因为我们都已经有了高等的抽象思维的能力.
   
抽象还给我们带来谨小慎微,感觉陷井很多.由于(思路,思维)无法沿着逆路回去,就不敢到原物质中去找证据.就象时间从原物质来,来到我们头脑里.我们(有的人)却无法到原物质(物体上)中去,以思维从桌子(前例)起点,沿途一直跟着光线,再跟着视网膜转换信号,再……..沿途一直亲自看到!!!然后有足够的勇气和底气______然后…….于是(在对自然界的认识上)混乱和纷争十分泛滥.对时间是由意识产生所进行的纷争便是一例.
   
我似乎想建立勇气和底气这样说:地球上的"时间"[之所以打引号,总觉得这个时间应该是意识的.如果不打引号,可能就是原来那个身份不明的时间],是经过这样的抽象过程(光素)和抽象能力(抽象思维)产生起来的;其它(星体)地方没有这样的抽象思维能力,就没有这样的"时间".[至少没有我们(地球上)这样的时间];其它时段,(比如混沌,及地球可能毁灭的以后),没有这样的抽象思维能力,
也不会产生我们现在这样的地球"时间".
   
从哲学上说,(哲学界已有人把)全部哲学都归结为语言逻辑.把文字语言分为0阶`一阶`二阶(<<语言逻辑哲学>>20页).现在,我们央网科技版正发生的,对"时间是意识产物"______的置疑,抵抗,及迷惑.之所以发生,(我说)是完全出在哲学的语言逻辑里,(对"时间是意识产物"不理解者)把:事实发生`抽象(思维)过程`词语(在脑内)形成,这三个阶段`这三个状态`这三个意义.模糊了,混乱了,或忽视了.鉴于主题篇幅问题,不展.
     
并且,本网版内,有网友"时间并不是一个和质量及运动并列的范畴"`"时间都是人以自已的需求给我们认识的世界所加注的"`"时间只是我们为了方便计算生活的步骤所制定出来的"…….都很肯定和醒目.他们的理解一定比我更深更广泛更高明.我相信我们绝大多数人的灵性都是准的.我们生活在一个准确的时间里对生活有准确的信心..当然,还有人说,"时间是人给地球涂的绿油漆"!!,虽然那个了些,却也有趣,令人深思..______在运动和意志(意识)之间……
    3, 时间梦幻
   
时间在我们身边慢悠慢悠地流淌,与我们随风而行.看梦幻吧,"我今天的日程排满了,上午去文化宫听课,中午参加一个朋友酒会,晚上还要演出…….";"我们必须赶在天黑以前翻上那个山坡,才不会摸黑进屋.";"你排必须在拂晓前占领5号制高点,封锁敌人退路……下命令!!.".我们警告罪犯:"你没有多少时间了!";与人商榷事:"我需要时间考虑考虑";交待任务:"你抓紧时间办吧.";人情交往,"唉呀,我一天忙得没得时间来看你";见面打招呼,"最近忙啥呀,好长时间没看见你了?"闲得的人说,"唉呀,这日子不晓得一天怎么打发!"梳理情感,"时间过得真快呀,一晃就是十六年了";峥嵘岁月,"把失去的时间夺回来!";催促学习,"一寸光阴一寸金,寸金难买寸光阴";唱悠悠的语句,"光阴似箭,时光任荏";上世纪有一首歌唱"我们要和时间赛跑………[赛跑?赛跑对象_____时间在哪里都不晓得呢],有趣.;(方言)"一哈哈儿"[一会儿的意思],"转过身就不见了"`"贬个眼的功夫"_____这些时间语言来得模糊,却很实用,生活中必须要有这样的时间语言,否则,人与人之间无法粘连.
     
在生活中,我们每每总有珍惜时间的思念,总觉得自身行动或他人行动,还不如人意.所有对时间的紧迫感,都出在对我们身体行动速度的紧迫感______希望身体行动速度快些快一些.象闪电更好(这一点有个潜机:头脑思维速度正是闪电).甚至有时能分身更好,同时在几个地方干事.我在梦幻着的时候就觉得:时光在流失,岁月流逝.或觉得时间很悠闲.或觉得时间是那样充足而又无限供应._____幻想这,时间是宇宙天空开启的门?幻想于,我明显地感到时间骑在地球这个园球上滚动.时间存在使我感觉地球好象永恒.甚或,时间的存在使我感觉地球就象宇宙的中心了那样……..我们人人都有梦幻这些时间的能力,我们都是常人,我们人人都能了解和体验时间.但是,议论到份儿上,大家都不知道“时间”是什么了.就好象头脑长在自已身上,却不知头脑为何物.用得来,说不出来.看得见,模不着.让我们灵魂困惑!兴许,我们人人都不习惯于“打开”自巳的脑.哪怕借助知识借助介绍_______来打开.于是,本来就装在脑内的(时间),我们就说______不在脑内了.“那怎么行?”,“那不唯心论了吗?”那不这,那不哪.如此,如此.因而使得,我们有时(很多人)对时间这种了解和体验,停留在:只能意会,不可言传,的程度.
______这是因为,放弃,遗忘,省略了抽象(放弃遗忘省略把抽象思维用来思考这些).或抽象思维能力从来就不足,(我自巳常常对自已的抽象思维能力感到捉襟见衬.)
______由于放弃遗忘省略,(或别的什么原因),就出现了把"只要有运动…….没有人的星星……."说成也有时间的怪事.愚弄不浅.
     五 时间三解
解一:模糊时间法_________拂哓,黎明,清晨.中午,正午,响午.傍晚,黄昏,擦黑.深夜,半夜,鸡叫头遍.这个方法是模糊时间,其由来及演变想必很古很古了.这法全凭人的习惯感觉,约定俗成,估摸着办.
     
解二,简易技术法_________(中国特产)子丑寅卯辰巳午未申酉戌亥.12个时段,子时约凌晨0点,从子到午6个段,6x2=12点.午时为12点.每一个段为2个小时,全天12段,24小时.这是在夏至那一天,以正午太阳位置,通过特置光孔投射,确定太阳正当顶时为午时.再以沙漏或水滴法,测定全天12个时段,再命其名为子丑........这个方法,利用了简易技术,分段精确度差,使用起来不方便,细分时不够,满足不了当代生活需要,更不用说现代工业化的分秒计时需要.
     解三,现代科技法__________24小时.将地球分成24格(经线),确定国际日期变更线..
________至于说(有人提出)"洗澡时间感觉"`"忙闲不均时间感觉",这本身很有趣,也由此引人对生活思考和联想.但巳属于人的情感范畴.不是科技范畴.(他又)"再论时间的本质"也没道出时间的本质.硬牵扯过来.总之,是对(时间的)

概念消化不够.
     
而,即使这样(三解),也还有人躲在宇宙深处发笑呢?!他笑我们地球人受骗啦!他说,你们地球上的24小时,那也是地球独自创造.因为地球的旋转(自旋和公转)没有对照物哦!无法"相对论"?地球的运转没有公证处与监督机构,谁知道它旋的准不准呢?
_________可是,我们(地球上的人类在时间取样定基准上)除了受地球的"骗",(还)有别的办法么?....
   
    六,
    时间可以看到
   作者:思维.永恒3 时间:2003-10-16 08:25:16__________回复某网友"时间可以看到吗?"的诘问
可以用眼睛看到时间.
   
可以用眼睛看到那些以一定形式表现出来的时间.时间(源)本是一种概念(在意识内,在头脑内),当这种概念以一定形式表现出来的情形下,我们就看到了时间的表现形式(和情形).时间既不是物体本身,也不是运动(运动其实巳为概念了,对一种情形的概念)本身,也不是"能量"什么的..
     当太阳升起来了,我们看到了早晨的情形,我们概括成早晨(时间概念).当日头当顶,我们看到了响午时间.当太阳滚下山那边,我们看到黄昏来临.
     
当打开世界地图,我们看到了(早已将地球运动情形转换成概念并刻写[和划]在地图上的)国际日期变更线,当用手旋转地球议,经度上的时间就在跑........在,大型宾馆,飞机场候机厅,都挂有让我们看得到的:东京时间,北京时间,伦敦时间,纽约时间.........这些时间概念的表现形式.
    时间是用刻度和标记制定出来的,就是让我们看得到.
   
时刻,时刻,也许"时刻"的思考意义就含在里面.时间是以物体运动为背景,刻画出来了,难道不是么?看不到的,始终也拿不出来看到的,不是现实时间.看不到的时间,要么是思维游戏,要么是历史,历史(也)不是现实.或将来......(将来是非现实的未知).
所有没有人(人类参与,知觉,认识)的时间,不是现实时间________顶多是种科幻,充其量是预言.
时间,时间.它(这个主题)与本科技频道"延续与终极"`"隔空移物"相对照,这个主题是躲不过人们现实眼睛的.某些科研用到的时间,也在科研手段的现实中.
   
所以,眼睛看___________有人感叹地说,“人事的变迁莫过于时间的伟大”.时间,在我看来,这表明了物质不永恒,物质在运动.时间的本质是对物质运动过程的记忆,时间并不发生在物质上,物质与物质间的运动也不会显示出时间.因为,所有物质没有记忆,没有记忆就不能联想,不能思考,不能综合与判断.不能判断就不能定格出时间.只有人这种具有物质与记忆两种属性的存在,才能定格出时间.时间是人意识的认定.人也有作为物质的属性,人也在运动.旅游运动到这里到那里,工作调动到这里到那里.这两种都有身体物质运动在里面,旅游身体物质运动为主要,身体物质变迁为主要.工作调动身体物质运动占次要,身体物质变迁为次要,思维内容变迁为主要.因思维内容是寓载在人体物质里的,所以,思维内容关于工作的变迁还是要身体物质运动来实现.只有写作恰恰相反,恰恰不需要身体物质移动,这样才能保证更好的思维运动,思维内容变迁,所以,在现实中,因运动带来的事和物之间关系改变,即时间变迁,在人们心中叫做“苍海桑田”,叫做物换星移,斗转星移.叫做“三十年河东,三十年河西”.被称为人生轨迹.所以,时间的伟大莫过于记忆的伟大.只有记忆才可能在地球上发生理解时间,应用时间的事实.

    七,
    时间灵魂
    作者:思维.永恒3 时间:2003-10-20 21:20:14
    这个被我们地球人叫为时间的是个什么东西呢?地球上原本没有时间,地球不产时间,至今地球也没有(它的)时间.
   
但是,在人类生活中,时间的词语却很多,时间概念很多,用场很多,时间很重要![在这里,语言词是从脑内发表的.概念是在脑内没发表的],[并且语言词包括文字词和语音词.概念既可以文字发表,也可语音发表].形形色色的“时间”都出自人的语言文字,那么,“时间是人给地球刷的油漆”简直太形象不过了.在人们的声音中,在人们制作的文字中,在人们制作的钟表上.还有一部分留在人的头脑里.这(“油漆”)颜色存在于人们的意识形态中,人们感觉它存在就存在,感觉是什么颜色就是什么颜色.
人们为什么要产生时间?  人们拿什么,依照什么,参照什么产生时间? 沿着这两个“?”`“?”号进发,我们就可以找到时间的灵魂!
    1,人们拿什么……产生时间?
    人们以自己的头脑为工具,以地球运动_______这信息为材料     
地球运动(自转和公转)这一物质现象,是以信息身份进到头脑里来的,而不是地球本身和运动形式下的地球本身.光线是唯一媒体,光线本身(即使所谓光线在这里作了运载工具,蕴含了信息量)是无言的,无语的,无意识的,把这种光线,解释(理解,诞生)可以用语言表达的时间,
用文字表达的时间,存在于意识形态中的时间,是人头脑的智慧.时间作为一种信息(也既不是地球物体本身,也不是运动本身),完全是由人的意识产生的,把现有信息(除非信息源泉,地球运动及其相关环境变)创造成人们需要的时间信息(包括语言文字时间和常驻于脑内脑的时间概念)完全是以人们的意志为转移的,包括时间的形式和内容.仅从形式上,比如我(世界当初)把一天定为“48”时节可不可以?只要在地球经度上每15度一格处标明为2时节即行.换算单位和名称变了,地球在每一经度格运动(自转)的角度仍为原发生事实,地球转360度的自然事实谁也没去改动一下,也改不了.再把每2时节细分(成一个什么名称一些什么单位)即行,亦或把每7.5度分成一根经度线间隔,则每一格一个时间。。。。。无穷.
    现在24小时分法,1小时60分,1分60……..是根据人的意志,更适合人的生活和心理习惯,更适合现代工业生产和科技研究,而由智慧(意志在其中)凝结晶的.
   
人的智慧创造了(用于语言文字和贮于脑内)的时间概念,时间概念(语言形式,文字形式),由人的意志决定的是这种(关于时间的)信息的形式和内容.只要你记住意志决定的是信息,而不是物体及物体运动,就不必为此而惊慌了,而神经衰弱地滑到唯心唯物什么的上面去了.
   
意识创造时间,只是创造概念本身,并不(没有)回到地球体运动上去哦.意识对宇宙的认识,也只停留在(每一个人的)头脑的概念里,并不去触动一下宇宙(各星体)的原物原貌啊.还有什么唯心不唯物的帽子敢拿来?!!真正是神经衰弱.
   人的意志决定时间的内容.
人的意志是人的灵魂主要象征.人的意志(于是就)是时间的灵魂.人的灵魂拿地球运动的事实为依据,再按照自己的需要产生时间,产生了文字的语言的(以及仍存于脑内的概念)时间.脑内概念本身还是关于时间的概念,这时概念是其性质内容,语言文字是其形式,而仍存于脑内的概念由于没有表现形式只能谓概念.
     2,人们产生时间是为了人的行为需要.
   
人是一个可以移动之物,人除了移动自身(也说自身物体有什么不防?)身体,还移动地球上别的物体(物质),这“二动”均为行为,要行为得有个相关连系(也包括人与人的连系),要连系就得有个运动次序.时间就是来为这个次序定一个抽象的格.因为人发生了行为,还要发生行为,发生了一个行为过后还要再发生一个行为.人的一生要发生很多很多行为……..人的行为不会在“奇点”发生完.象宇宙大爆炸于一个“奇点”那样……..于是“过程”(人生的过程)就免不了.于是,有过程就有次序先后,有先后,就有了时间来描述(记忆,记录,记载)这先后.所以,时间是人对己行为的定格,有一个抽象的格位,便于准时(准确)入格,准时于8:30到达办公室…….以及记录下某一时刻发生大事……..
    而钟表,则是对这一个抽象格位的有形表示,人们按照这有形的格位办事(发生行为),但当我们按照这有形格位办事时,不要忘了它还是人(地球人)的灵魂产品哟
时间的灵魂,时间(词)本身无魂,时间(词)无魂而诞生,无魂而来.它的远祖在地球球体运动本身(相对于太阳的运动),是人的灵魂一手把它从一些普通的光线转化成时间的.所以时间的灵魂,就是灵魂的时间.地球太阳和所有天体一样都没有灵魂,只有人才有灵魂(意志).
     灵魂的时间!!————它就是这么一个东西.
   
     八,
    时间存在
  
   
首先存在于意识中,我们(我们人类中的科学者)已这样的(时间意识)意识去解读世界(宇宙),认识世界(宇宙),开发世界(宇宙).比如“神舟五号”上天的全过程.无不充满了,以地球24小时时间为单位的计算设计.诸如轨道飞行,速度控制,程序转换等等.“神五”在天上运行了21个小时.这个21小时就是地球上的时间。(地球的24小时时间)而当“神五”在太空运行时,它不可能自定时间.科学家不可能给它创生一个时间.其舱内,每秒多少公里的速度(表针)也是在地球上,按照地球24小时标准产生的.地面上,再根据多少秒,多少分,它将运到太空中什么位置,发出什么指令,.这些操作指令,都是以24小时(60分,60秒)的常规来计算的.计算后,再加太空的修正量……..地面上其它科技与生产的各种时间与计算也都要加计算修正量.
没有24小时的基础,就没有对“神五”太空运行作计算的前题.还有,对生物的生命时间观察,也借代了24小时分秒微秒时间.
对考古物碳14测定和同位素测定,也是以24小时作参照系的(换算单位标准)..24小时与”碳14测定的结果之间,24小时与同位素测定结果之间,存在一种相对应关系.[相对论吧].由这种相对关系就得出时间的论点_______该古物[相当于于]多少多少年历史.我们实际解释时都省去了”相当于”,而直接解释为古物多少年历史.
   
其次,时间存于各种形式中:有钟表的,机械钟,电子钟.木壳钟,座钟,桂钟,闹钟,报时钟,手表,怀表.有文字记载的,列车时刻表,课时表,会议日程表………还有电话座机显示的间,电视机字幕时间……时间的这些形式,万变都不能改变其时间灵魂.其形式仅因为有其内容才有意义.若不是这样,那它就(仅仅)成了装饰品.
   
再其次,时间(以概念)存在于人们的脑内.我们平时,总是强调,某人工作要负责任,要有时间概念.又比如,某某艺术节会议,倒计时了.申奥冲刺倒计时了,这些概念的提出,都是在重重地,强调人们头脑内注意此时间概念,树立此时间概念,提醒这一时间概念.由这些概念在(各个)脑内的运行,来激发统一的整体力量.有了这样的注意,树立,提醒,人们(各自独立的)意识,就会受时间概念的驱动而演译出意识决定行为的集体辉煌..
   
这里假设.意识和概念略有区别.这里的”存在于意识中”所包含的意思是:脑内的时间概念,已经与脑内的其它信息结合,结合了的意识,即可以指挥行为.而”存在于概念中”则尚未进入与什么结合.尚处于单独存在状态.要俟结合后才能生效指挥行为..存在于各种形式:.意识的,概念的,钟表的,文字的,_______24小时时间制!!!由于所有的时间都要在24小时制中,来找换算标准
.所以宇宙中除了24小时时间,再也没有别的时间了.时间(24小时)的存在,决定着人们的生活序列.约束着一个庞大社会机制的运行.也表明着各个历史运行进程的状态……..
     
那些,通过思想和情绪,精神和意志,去创造时间(概念).这,是一个对(现实存在)”时间存在”的延伸[延伸到人类以外]与发挥[发挥到情感中去].是对舆论的愚弄.(科技方面的愚弄).
    十
    思维方式一瞥
有一个帖中,有一句“`地球早晚`怎么不是时间呢?”(当然还有好几句).这单引号中的话是引自2003`9`26,12:!0帖<<且不鱼目混珠>>中.引用目的,
是想证明地球洪荒也有“时间”.在9`26帖中, 原本有一个基本观点:发生了才是事实,什么时候发生的就是什么时候的事实.余此,可以瞥见一个思维方式.
   
首先,9`26帖中阐述的,(原始社会吧)有人说了第一句“哦,太阳落山了”,“喂,该吃早饭了”.这两个语言都有地球绕太阳运动的背景,它符合了今天24小时制时间的体系.在原始人只用这种语言表达时间就不错了.所以叫,这样就发生了“时间”事实..
     
其次,为什么又必须有人说出了第一声“哦,太阳…..”,“喂……早饭”了,才有时间呢?因为,没人说,就等于地球上从没发生(地球在宇宙中,也是宇宙没发生),通过语言表达时间的事实发生.地球无嘴,它说不出“喂,……早饭了”的时间,它请,山涧泉水唱的那几首歌.我们反复听,也听不出语言..地球是文盲,它无钱请识字先代写文字(时间的文字).它本想拿录音机去别的星球录点包含时间的谈话,又无录音机.也无传真机,架个线接受宇宙中不知什么星体传来的反映了时间的传真文字.更不消说办工厂制造钟表了,办工厂要头脑…….它没有头脑,它把全部希望都寄托在人的头脑了,人是大地之子啊_________
     
再其次,(1.6.23:56帖中语)“他所说的`地球早晚,日起日落,地球旋转,是在地球从混沌洪荒到类人猿阶段也发生了的”.于是23:56帖中就抓住这一句“地球早晚”,下狠心断定
  
______“这怎么不是时间呢”.以此反推:你自已都在说原始人(或类人猿)以前就有时间了.[可惜,这一句话原本并不能作为他的论据,他(却)拿去作了论据.].
     
这是一个荒谬!!!这个反推是一个荒谬的逻辑!荒谬在于什么?__________在于,(对于,或受了)哲学的蒙蔽,蒙蔽的哲学.(对于,或受了)哲学的误解,误解的哲学.
   
谁都知道,(比如)考古挖出了人骨,考古学根据检测这人骨的性别,年龄,死因,并配合随葬品推测死者生前地位乃至死亡年代时间……….当学者们在议论与这块骨头有关的时间时,是议论在场时发生的关于”时间”的事实.并不代表死者下葬时也作了这一系列议论,也要作性别检测,(滑稽:他们知道性别,还检测啥?)也就是
     十一,
     思想速度
    换一种理解_______回"大印"02_01 10:57帖作者:
    思维.永恒3  时间:2004-02-09
17:07:11对网友“思想速度”的换一种理解.“思想速度”原本是存在的.思想,无非思考,回忆,逻辑,意志,情绪,意愿……..等等脑内信息运动形式,包括存入(记忆),取出(回忆),组合(思考等等),等运动形式.这些信息运动,信息总是(在脑内)从一个物质到另一个物质,这在脑内距离之短,已短到了极限,这速度之快,医学解剖已有充分理由证明它们是电速!极快的速度在极短距离完成的“思想”,你说思想速度快不快!!
     正因为快, 所以, 我们才有一闪念, 才有心驰神往, 才有思想灵活,
才能实现机智与敏锐……这里换了一种理解的,思想速度不是抽象的思想速度.在他们那些讲的是抽象的思想速度.抽象的思想速度会有很多困惑与无知(取无法知道之意,
绝非通常的贬意.
望凉).他们那种讲法的抽象思想速度和抽象逻辑思维,会使哲学家先生们痛苦不安……..在理论里打架斗欧……….在理论里纠纷绵绵………在精神空间里永……
     十二
     时间切磋
     作者:思维.永恒3 时间:2004-07-29 09:02:44
_______这是针对网友“我是极”一帖中内容切磋.从能量到以论时间为主.
   
“物质是能量的凝固状态”似乎为不要空间而只有能量的世界,找到一点出路.任何一个物质里的电子都在动(自旋和绕核公转.质子与中子及种微物质都在发生着力的聚合与对抗.原子弹释放的这种力量,核电站也是.所谓核聚变,核裂变.所有未能象铀等那样成为核能开发材料的物质,并非无能量.我想,可能是它们没有铀等那样易于开发罢了).
     但,把“时间”也说成是承载能量的场,这绝对是死胡洞,没有出路.
     何以见得!因为,时间压根儿就是人头脑里“自已”产生的,[注意,是整个为人的人类,绝非某一个先知先觉,若那样,就唯心主义了],它,哪来场,何沦为场?
     关于时间(及时空)的这个误区,是很多聪明的人都正在犯着……
     这恰恰就是,人缘于意识,又蔽蒙于意识.越聪明的人意识越多越厚越深…….由于意识越多越厚越深建立的“我圈”………[而,相比而言,我确是个笨蛋.].
     最简单的启蒙就是:有人站在小木船上,用竹竿撑自已船头,能用力使船前进吗!
    所以我们的时空宇宙有限无边,而那个真空宇宙则无边无界也无始无终,从时空是和能量一起从极点中迸发出来的现象看,这个真空宇宙里能量可以以真空方式存在。
     
你可以不要.除非傻瓜,傻瓜头脑里什么概念也没有.正常人,你想不要时间概念也不可能.清晨,你从梦中醒来,时间概念就找上门来了.(己有了常识的)人一舒醒.时间概念就从大脑仓库里取出来摆在运算平台了.....现实生活中也有不要时间的."不分白日昼夜",便是一种一定程度上不要时间现象.
     "人可以不要时间"这一句话,从逻辑上不成立.因为,"时间"包含了两个语言阶段_______物质阶段,意识阶段.
     人只可以不要这一个阶段,不能同时又不要那一个阶段. 所以,原句不成立.
     十三,
    时间是个大问题
    作者:思维.永恒3   时间:2004-07-30 18:46:57
    时间是个大问题. 它决定了物质运动. 决定物质运动的间隔, 没有间隔(运动)的物质是封闭的. 决定物质运动的继续, 没有继续运动的物质是死的.
    那么,是什么推动物质运动有间隔,运动能继续?就是时间本身吗? 可以想见, 时间,对我们人来说,是从A_______到B.
时间,对于意识(思维)来说,是信息转换. 从A______到B的时间更长,思维信息的转换时间极短,但极短也绝不为0.
    然而,其实, 在物质世界没有时间.. 由于有记忆才有时间.人从A________到B,到了B点后, 还记忆着A点.所以,就有这个时间.
而在物质世界,物质世界本身对自已的行为是没有记忆的[山体记忆了冰川时期冰川擦出的痕迹,但并不将信息交流起来思维.或者说,人在有限生命内实现了信息的立体交流.这一点目前已知的宇宙中除人以外再无第二],物质世界不记忆过去,也不计划将来.它们的运动是不确定的,所以,
就用不着(也不可能)计划将来. 物质状态(运动状态)的本身不确定性, 也就无法记忆..
所以,因而.人因为有记忆_______而意识_______而思维,思想,立志,知识,文化等.但是,所以,因为.人的记忆不说就是短暂的吗?一般六七十岁,充其量,204岁(据说匈牙利曾有过的一位世界最长寿者),就短暂而结束,成了消失的永恒.
成了“不可再返”的短暂.至于人类本身的文化沉淀数千年.那是另一种“记忆”形式,另一种物质“运动”.
    所以,我们不要拿我们得到的世界的知识(时间概念)去诱骗物质世界也有“时间能量”!!!
    若令时间有能量,这将是100%的谎言.
   未了,“我是极”网友,在这一点与您针锋相对,你大肚能容量吗?顺致谢!!
   
     十四
     驳时间倒流
______有网友说时间可以倒流,象岁月流逝了,又可以流回来……
    作者:思维.永恒3 时间:2005-08-04 18:28:20
    1,时间不是物质.
    2,时间只是概念.
    3,概念只是头脑信息变幻的形态.
    4,头脑信息形态的变幻已经不能要物质运动同步.
    而倒流必须物质运动.所以,
因物质运动而人可塑性思维产生的时间概念不会发生倒流.即时间不倒流.4,人的可塑性思维已经超脱了造物主的原意.______a,可塑性思维在脑内的信息活动不再受地心引力限制,信息活动在脑内交流不分上下空间,上下交流平等.b,可塑性思维不再受太阳光限制,白天黑夜均可思考.c,信息在脑内运动也是物质.只因这物质太轻,引力巳处于忽略份量.所以不受引力限制.
    5,正是由于这些原因,才使有人误为时间可以倒流.



1<词>,2[句],3/段\,4{节},5(章)。
2016-2-3 19:43
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 13 楼』:  12Linux下的经典软件(史上最全) 13楼

12Linux下的经典软件(史上最全)  13楼
Linux下的经典软件(史上最全)
标签: linux软件经典史上最全
2016-01-30 10:30 36人阅读 评论(0) 收藏 举报
分类:  linux(18)  
目录(?)[+]
转载说明:
一直想对linux下的软件做一个大汇总,包含各类型软件的推荐、介绍、安装,以免新手走弯路。不过发现网上有大神总结了一份很好的文档。以下内容转载自:http://blog.csdn.net/lnxfei/article/details/45821925

前言

从2012年接触Linux系统以来就被Linux系统所吸引,2个月后便完全抛弃了Windows。在这2年的时间里,我尝试了很多Linux发行版: Gentoo, Fedora, Ubuntu, Debian等。在这些系统中又尝试了很多种软件,这里根据自己的使用经验并结合网上的一些资料,对Linux下常用的软件进行整理,供大家参考,希望能帮助到大家。每款软件都有它的优缺点,适合自己的才是最好的。在这篇文章中我是按自己的喜好推荐给大家或者进行排序的,并不是说它们就是最好的,其它的就不行。建议大家多尝试几款同类的软件,找到最适合自己的。我虽算不上Linux新手,但也不是什么大牛,所以文章中有不足的地方,还请大家多多包涵。

网页浏览器

工具

Firefox

Linux下最成熟也是人气最高的浏览器,有众多的插件可供选择,还支持支付宝。Firefox一般是Linux系统自带的默认浏览器。Firefox对各类网页的支持非常好,而且非常稳定。这也是我最喜欢的浏览器,唯一感觉不方便的是快捷键不能自己定制。最新的Firefox 37.0.2还有视频功能呢,边工作边偷摸的跟媳妇儿视频,感觉是不是很棒?Firefox还有开发工具,比如著名的Firebug.

Firefox有一个vimperator插件,允许你用Vim的快捷键来进行一些操作。喜欢VIM的朋友可以尝试下。Windows下还有款Waterfox(水狐)浏览器,有意思,这事要跟Firefox对着干的节奏吗?

Konqueror

Konqueror是KDE桌面系统的一部分,主要用于文件管理、各种格式档案的查看,以及网页浏览。它有以下区别于其它浏览器的优点:

支持用Firefox, Chrome打开网页

丰富的快捷键功能

Web浏览器, 文件浏览器于一体

运行速度非常快, 快于Chrome

跟KDE结合的非常紧密!

Konqueror“狗皮膏药”。konqueror称它们为”access key”,按ctrl键之后显示出来,用户只要再按一下对应的按键,就可以跳到对应的链接去了。

当然它也有一些缺点: 没有Firefox, Chrome稳定,有些网页支持不太好,但不多。有时候还会占用很多的系统资源。

Chrome

Chrome是Google公司开发的开放源代码的网页浏览器,是一款成熟且有发展前景的浏览器,毕竟它的东家可是Google。Chrome最大的特点是简洁高效,速度快。因为Chrome在我的Fedora系统中有些网页的字体显示有问题,所以目前我把它作为我的第二浏览器使用。不得不说Chrome现在有赶超Firefox的势头。使用Chrome浏览时,还可以充分利用Google的功能。将Chrome与Google结合使用时,您不仅可以获得相关度更高的查询建议,还可以利用各种Google产品(包括语音搜索和Google即时)的功能。

Opera

首先Opera不是一款开源的浏览器,但它是免费使用的。Opera给我的最大的感觉是定制性比Firefox, Chrome强太多,尤其是快捷键,你几乎可以给任意想要的操作分配快捷键。虽然插件没有Firefox多,但是不得不说Opera的功能是非常强大的,我最喜欢它所集成的邮件功能。Opera是Linux系统下又一款不错的浏览器。我把Opera放在我的第三个选择上,原因是Opera不稳定,有时候这种不稳定已经影响到我的工作。期待Opera能够越来越好。

Opera也有手机版,手机版和电脑版之间可以同步书签。

Seamonkey

这是一款自由开源、跨平台的互联网套装软件(包括一个Web浏览器,电子邮件和新闻组客户端,一个HTML编辑器,一个IRC聊天和网页开发工具),由Mozilla基金会创建,之后转由旗下的SeaMonkey项目领导团队开发。它是Firefox经典版 + Thunderbird经典版的组合。

Conkeror

这是一款非常有趣的浏览器。写在这里主要是因为它的操作模式非常不同。Conkeror是为Emacs狂热粉丝准备的。通过它,你可以以Emacs之道来畅游网络。用它来操作网页和用Emacs来操作文本非常非常的像,如果你是Emacs控的话,推荐尝试下。个人感觉它只能作为一款初级的浏览器,主要原因有两个, 一个是它对很多东西支持不是太好,比如图片和图像。另一个原因是因为它有很多Bug。其实还是蛮期待这款浏览器的,可惜它的社区力量有点薄弱。

Orphne

成人浏览器,你们懂的。官方网站:http://orphne.sourceforge.net/main.html 感兴趣的童鞋自己试吧。大千世界无奇不有,Linux世界如此的精彩和多样。

其它

dillo

一款小巧的网页浏览器(源代码约420 KB,二进制程序约350KB。),遵循GPL协议。用C语言编写,使用了GTK+ toolkit,该浏览器特别适合运行于老计算机,以及嵌入系统。

选择

KDE环境下: Firefox > Chrome > Opera > Konqueror > Seamonkey

非KDE环境: Firefox > Chrome > Opera > Seamonkey

文本界面的网页浏览器

工具

文本浏览器我平时用的很少。如果用也只是用w3m。w3m是一个成熟、稳定且强大的命令行web浏览器,在各个发行版上都能稳定的运行。其实命令行的浏览器,用习惯了都差不多。w3m对中文的支持应该是最好的。elinks和links对中文支持都没w3m那么好。

比较有名的应该就是w3m, lynx了,elinks也不错。当然还有其它的比如links, links2等

选择

w3m >lynx > elinks >links2 > links

聊天软件

工具

因为我平时主要通过QQ,IRC和Skype聊天,所以这里只介绍QQ,IRC和Skype相关的客户端软件。IRC的客户端软件其实有很多,功能都差不多,所以找个界面舒服、功能多、稳定的客户端就可以了。至于QQ,在Linux中用的最多,最好用的还是Webqq。至于腾讯发布的QQ for Linux(linuxqq), 大家还是忘了它吧。Skype客户端就一个,那就是Skype。

Xchat

Xchat是非常流行的IRC客户端,利用它你可以登陆到任何的IRC服务器和别人交流! xchat运行在X11环境下,有着良好的用户界面,和许多聊天所需要的功能,例如私聊、支持多个聊天室等等。总的来说Xchat给我的印象就是非常稳定和简洁,功能能满足我基本的需要,所以我基本用Xchat作为我的IRC客户端。

Pidgin

Pidgin(原名:Gaim)是一款IM即时通讯软件,支持除qq外几乎所有IM软件。功能很强大,界面友好,也稳定。它还拥有不少独特的功能。最流行 的要算是好友提醒功能了,当某个特定的好友离开或者脱机,它会用某种方式对你进行提醒,比如发送消息、播放声音甚至运行某个程序。所以如果不喜欢xchat,pidgin是个不错的选择。

Empathy

从Empathy的功能描述来看,比较吸引人的特性包括:支持多协议,语音/视频支持,以及强调协作等方面。

WeeChat

WeeChat是个基于终端的快速的轻量级IRC客户端,可以在多种操作系统中运行。所有的东西都能用键盘完成,而且可以自定义。看它的官方文档貌似很不错,如果大家喜欢在终端下使用IRC的话可以尝试下WeeChat, 当然还有其它能够运行在终端中的IRC客户端,但是貌似都没有WeeChat好用。

ERC

ERC是Emacs的一个插件,可以作为IRC客户端用,所有的操作都是用Emacs快捷键来完成的,非常不错,我基本上都是用ERC在freenode中聊天的。Emacs控一定要尝试下。

Firefox/Thunderbird IRC插件

Firefox/Thunderbird中也有一些插件可以作为IRC客户端,但都不好用,主要是有新消息来了,不太容易注意到。

Webqq

如果想在Linux下用QQ,Webqq是最理想的选择,虽然有些功能还不支持,但是绝大部分的聊天功能都支持的很好,最重要的是稳定。目前腾讯还在积极开发Webqq,以后的Webqq用起来会更舒服。

QQ for Linux

腾讯官方出的Linux版QQ,功能有限,Bug多,很久没更新了,腾讯也放弃了对它的支持,所以基本上可以说这款软件是废了。

Wine

相信想在Linux下跑QQ的童鞋都想过或者尝试过这种方法,当然我也尝试过,给我的感觉是中文支持不好,界面不好,Bug很多,有些功能还不支持,所以不推荐用这种方式来用QQ。

在虚拟机中用QQ

虽然说小题大做,但是不得不说效果非常不错。大家可以起一个Xen/KVM/VMWare/Virtual Box的Windows虚拟机,在里面装上QQ,使用起来跟在物理机上Windows系统中运行的QQ效果完全一样。

Skype

Skype是微软的一个聊天工具,有Windows和Linux两个版本,Linux版本的Skype功能强大,简洁,稳定。Skype也是我最喜欢的聊天工具,它的目的很明确就一聊天工具。Skype还支持视频聊天,效果不错。QQ我是越来越讨厌了,腾讯出于商业目的绑定了很多非聊天的功能,把QQ搞的异常臃肿,也是无奈。另外说下微软现在也在大力推广Skype,所以Skype还是很有前景的。

其它

Kopete

Kopete是KDE的一个子项目,支持多协议的即时通信,包括ICQ、AIM、Gadu-Gadu、IRC、.NET Messenger Service、Yahoo! Messenger等协议。

选择

IRC客户端: pidgin > xchat > empathy > WeeChat > ERC > Firefox/Thunderbird IRC插件

QQ客户端: Webqq

Skype客户端: Skype

Email 客户端

工具

Linux下的邮件客户端有很多,还有一些是适用于不同桌面环境的,比如KDE下的KMail, GNOME下的Evolution。在众多的邮件客户端中最好用的当属Thunderbird和mutt了。

Thunderbird

Thunderbird是由Mozilla浏览器的邮件功能部件所改造的邮件工具。应该是目前Linux系统下应用最多,功能最强大,稳定性很好的邮件客户端了,支持垃圾邮件过滤、反“钓鱼”欺诈、高级安全等,可进行个性化配置。这个是我目前的第一选择。

mutt

Mutt 是一个很小型但功能强大的,使用文本界面的MIME邮件客户端,Mutt具有高可配置的特性,适合高级邮件用户使用。喜欢在终端下管理邮件童鞋的首选。

其它

Gmail

Kmail

选择

命令行: mutt

图形界面: Thunderbird

下载工具

下载工具很多,没有什么好不好的,看个人喜好。这里推荐几个常用的。

BT下载工具

kTorrent

KTorrent是KDE下的一款BT下载工具,具有速度快而内存占用小的优点,设置也比较简单实用,感觉和Windows下的uTorrent不相上下。

rtorrent

一个Linux下控制台的BT客户端程序。

非BT下载工具

wget

wget默认在各Linux发行版都有安装,成熟稳定,方便。我一般用这个来进行下载。

axel

Axel通过打开多个HTTP/FTP连接来将一个文件进行分段下载,从而达到加速下载的目的。对于下载大文件,该工具将特别有用。这个工具主要特点是速度快。是一款非常不错的下载工具。

curl

它是对libcurl库的一个命令行工具包装。libcurl库中提供了相应功能的API,可以在程序中调用。curl使用URL的语法来传输文件,它支持FTP, FTPS, HTTP, HTTPS, TFTP, SFTP, TELNET等多种协议。curl功能强大,它提供了包括代理支持,用户认证,FTP上载,HTTP post,SSL连接,文件续传等许多特性。

选择

BT下载工具:kTorrent和rtorrent都不错,主要看个人喜好。

非BT下载工具:wget和curl的选择,主要看什么场景,一般的下载用wget, 主要是操作简单。如果需要用到特殊协议可以选择curl。如果想要下载速度那么就用axel.

curl和wget的比较

文件传输

工具

rsync

rsync是一款高效的远程数据备份和镜象工具,可快速地同步多台主机间的文件。rsync功能非常强大,经常被用作企业级的数据备份。rsync更适用于大数据量的每日同步,当然也可以用来进行简单的文件传输,但没有scp命令简洁。

scp

scp命令是SSH中最方便有用的命令了,scp就是secure copy,是用来进行远程文件拷贝的。数据传输使用ssh,并且和ssh使用相同的认证方式,提供相同的安全保证。这个是Linux下最常用的文件传输工具。

rcp

rcp不是一种安全的的传输文件的方式,rcp通过rsh来执行远程命令,要使用rcp必须经过一些配置,现在rcp已经被scp取代了,常用scp来进行文件传输。

选择

如果是传输简单的文件: scp > rsync > rcp

如果是用来做数据备份: rsync

FTP客户端

工具

lftp

比ftp好用,支持TAB自动补全。功能全,稳定。可作为首选的FTP客户端。

ftp

在命令行中ftp命令够资格,很实在。但是它不支持TAB自动补齐,这很让人头大。功能也没有lftp强。

FileZilla

图形界面的FTP客户端。支持Linux和Windows平台。个人感觉是最好用的图形界面FTP客户端

选择

命令行: lftp > ftp

图形界面: FileZilla

文件管理

工具

在平常使用Linux的过程中,为了管理自己的文件,恐怕谁也离不了文件管理器。Linux中有大大小小的文件管理器,有基于控制台的,也有图形化的;有单窗口的,也有双面板的;有轻巧型的,也有笨重化的。哪一款文件管理器最好? 套用一句广告词:“适合自己的就是最好的”。

Dolphin

KDE4中的默认文件管理器。Dolphin专注于文件管理本身,是我目前用到的文件管理器。

Nautilus

又称鹦鹉螺,是GNOME桌面环境中的默认文件管理器。虽然Nautilus稍显笨重,但是功能非常全面。Nautilus具有树状视图,支持通过脚本来扩展其功能,并集成了光盘烧录特性。

Konqueror

KDE3桌面环境中的默认文件管理器,在KDE4中被Dolphin取代。Konqueror集文件管理、网络浏览、文档查看于一身,具有多种不同的“身份”。

Pcmanfm

这款文件管理器还真不错,界面简洁,运行稳定、高效,支持多标签(这一点dolphin和nautilus都支持的)。是LXDE默认的文件管理器。

Thunar

Thunar是Xfce桌面环境中的默认文件管理器。它的优点是运行快速,内存占用少,很不错的文件管理器。

Gnome Commander

GNOME Commander是一个快速和强大的图形文件管理器,使用双面板进行文件管理,主要适用于Linux系统下的GNOME桌面环境。界面和操作都和Total commander相似的Linux下的资源管理软件。

Rox-filer

用fvwm,xfce的人经常拿这个做桌面和默认的文件管理软件,小巧快捷,但使用和常规的文件管理软件不同。

ranger

Ranger是一个控制台下的文件管理器。Ranger用Python完成,默认为使用Vim风格的按键绑定,比如hjkl(上下左右),dd(剪切),yy(复制)等等。功能很全,扩展/可配置性也非常不错。

Vifm

Vifm是一个基于ncurses开发的文件管理器,使用类vi的键盘操作方式。

Midnight Commander

Midnight Commander,简称mc,是一个基于文本模式的文件管理器。

选择

文件管理器,个人感觉,基本功能都差不多。所以选择哪个完全根据个人感觉,不必过于纠结使用哪款软件。如果想功能比较全,比较稳定的,那么就用你桌面环境默认的那款:比如KDE下的Dolphin, GNOME下的Nautilus, Xface下的Thunar。基于文本模式的文件管理器我最喜欢Ranger, Ranger功能多,而且稳定,操作非常方便。

文本模式下的文件管理器: Ranger > MC > Vifm

参考

推荐几款Linux常用的文件管理器软件

文本编辑

工具

文本/文档编辑器有很多种,不下于287种,它们都能完成基本的编辑任务,下面介绍在Linux下比较火的编辑器。其它的编辑器没怎么用过不做介绍。

vim

编辑器之神,定制性强,稳定性高,轻量但功能却很强大,所有Linux发行版的默认编辑器,用过的朋友都说好。vim应该是Linux下应用最多编辑器了。

emacs

神的编辑器,和vim一样都是我最喜欢的编辑器。定制性和功能要比vim强大很多,但多功能带来的是emacs要比vim庞大很多,启动的时候由于要加载很多东西导致启动比较慢,不过还好emacs有server模式,完美的解决了这个问题。在emacs里你几乎可以做所有的事情,写代码、读新闻、发邮件、写slides等,总之只有你想不到的,没有它做不到的,喜欢捣腾的童鞋可以去尝试下,没准你真的会喜欢上它。

xemacs

emacs 的X Window版本。

Sublime Text

Sublime Text是一个轻量、简洁、高效、跨平台的编辑器。定制性和扩展性非常强,非常值的一试。

kedit

KDE下默认的图形界面的编辑器,轻量,稳定,编辑器有的功能它都有。

gedit

GNOME下默认的图形界面的编辑器,轻量,稳定,编辑器有的功能它都有。

选择

终端模式: emacs/vi > sublime

图形界面: GNOME下用gedit, KDE下用kedit

在日常工作中我一般是vim和emacs有选择性的用,用emacs写代码,写文档。用vim编辑/浏览一些小的文件。

2/16进制/编辑/查看软件

查看软件

xxd

hexdump

编辑软件

vim+xxd

Vim来编辑二进制文件,因为Vim本非为此而设计,因而有若干局限。但你能读取一个文件,改动一个字符,然后把它存盘。结果是你的文件就只有那一个字符给改了,其它的就跟原来那个一模一样。

hexedit

HexEdit是一款非常好的十六进制编辑器(文本界面)

Bless

Bless是一个十六进制编辑器,其主要功能包括:支持编辑大数据文件及块设备、能够执行搜索与替换操作,具有类似Firefox的标签浏览特性、可将数据输出为文本或HTML、包含插件系统等等。

ghex

GNOME下的十六进制编辑软件(图形界面)

khexedit

KDE下的十六进制编辑软件(图形界面)

选择

hexedit > bless>ghex/khexedit >vim+xxd

PDF阅读软件

工具

okular

Okular是一个在KDE4下的PDF文档浏览器,基于KPDF开发。功能强大,稳定。KDE用户的首选。

evince

evince是一个支持多种格式的文件浏览器,如的PostScript,PDF格式,单页和多页TIFF,DVI接口,DjVu等等。它具有网页的缩略图,通过Gnome或基于GTK+印刷框架和范围内搜索文件。它支持显示的PDF索引和浏览PDF文件的加密。

选择

也是要看使用习惯的桌面环境了,gnome/evince 与 okular/kde 应该是主流,另外还有epdfview与appvlv可供挑选。但是这些阅览器自己感觉还不完美。我用的是KDE环境,所以选择了Okular,感觉很不错。

翻译软件

工具

goldendict

GoldenDict是一款不错的、与StarDict(星际译王)类似的词典软件。它使用WebKit作为渲染核心,格式化、颜色、图像、链接等支持一应俱全。可以屏幕取词,支持本地字典和在线字典,还支持维基百科和wordnet。

stardict

星际译王是跨平台的国际词典软件!它功能强大,实用性强,“通配符匹配”,“鼠标查词”,“模糊查询”等功能倍受青睐!

选择

Goldendict > stardict

文件差异比较工具

工具

kdiff3

KDiff3是一款用来对文件或目录进行比较/合并的工具,在比较时它可以同时针对两个或者三个文件/目录而进行。通过比较,它将文件/目录的差异按行加以显示。同时,KDiff3提供有自动化的合并工具,方便使用者进行有关合并的操作。虽然KDiff3主要为KDE桌面而开发,但是仍然可以运行于其它的Linux环境。甚至对于 Windows、Mac OS X,KDiff3也有相应的版本

Diffuse

可视化比较,非常直观。支持两相比较和三相比较。这就是说,使用Diffuse你可以同时比较两个或三个文本文件。能够直接在Diffuse中编辑文件。可以使用快捷键轻松导航。

Kompare

Kompare是适用于KDE桌面的文件差异比较工具。它允许你以图形化的方式来比较两个文件,并通过不同的颜色来直观的显示文件之间的差异。

colordiff

在Linux下,使用diff命令可以对文件进行比较,从而了解其差异。不过,diff命令的输出结果以同色显示,对于这种差异的表现可能不够强烈。好在我们还可以通过ColorDiff来加以改善。ColorDiff是一个Perl脚本,它通过不同的颜色来高亮显示diff命令的输出结果,非常显眼。

Meld

Meld的目录对比可以对比两个工程有多少文件不同,每个文件做过哪几行修改,非常直观。好东西~

vimdiff

当远程工作在Unix/Linux平台上的时候,恐怕最简单而且到处存在的就是命令行工具,比如diff。可惜diff的功能有限,使用起来也不是很方便。作为命令行的比较工具,我们仍然希望能拥有简单明了的界面,可以使我们能够对比较结果一目了然;我们还希望能够在比较出来的多处差异之间快速定位,希望能够很容易的进行文件合并……。而Vim提供的diff模式,通常称作vimdiff,就是这样一个能满足所有这些需求,甚至能够提供更多的强力工具。

diff

非常常用的对比命令, 别说你没用过。

Beyond Compare

Beyond Compare是一款不可多得的专业级的文件夹和文件对比工具。使用它可以很方便的对比出两个文件夹或者文件的不同之处。并把相差的每一个字节用颜色加以表示,查看方便。并且支持多种规则对比。对软件汉化者来说,这绝对是一款不可多得的工具。该工具有Windows和Linux下两个版本

选择

其实每个工具都各有优缺点吧,这里是我的选择优先级,仅作参考:

文件夹比较: bcompare(Beyond Compare) > Meld > Kdiff3

文件比较: vimdiff > diffuse> diff/colordiff

当然非常简单的比较直接用diff就好了。我平时用的最多的就是vimdiff和kdiff3

Kompare没用过不作介绍。当然如果仅做简单的比较其实这些工具都是很不错的。

音频播放器

工具

Audacious

Audacious是linux或其它基于linux系统上的免费播放器。我比较喜欢它的稳定和简洁。占用资源也比较少。推荐大家试一试。

Rhythmbox

Rhythmbox是一个伟大的linux版本的音乐播放器. 它可以容易的帮你组织音乐内容,并且是免费的. 它的灵感来自于苹果的iTunes,它使用GStreamer多媒体库开发,在GNOME桌面环境中执行结果和效果者让人感到惊艳。

Amarok

Amarok是linux和unix上另一个伟大的音乐播放器. Amarok的界面非常直观. 它是免费的自由软件。KDE用户的不错选择。

XMMS

XMMS可以称得上是Linux下优秀的音频播放器,是专门为X-Window设计的版本。目前几乎所有的Linux发行版都预装了XMMS。XMMS以强大的播放功能、多变的皮 肤和各具神通的插件在众多的Linux播放软件里占据重要地位,完全可以和Windows下的Winamp相媲美。现在不推荐使用。因为你可以选择比它更强大的播放器。

foobar2000

我最喜欢的播放器,可惜没有Linux版本,之前在Wine上试了试感觉很不错,如果是foobar2000的忠实fan可以在Wine上试试。

其它

Banshee

SongBird

GmusicBrowser

Bmpx

选择

Rhythmbox,Audacious,Amarok这三个应该是Linux下重量级音乐播放器,大家任选一个吧,都很不错。我都是换着用的。KDE下Amarok应该比Rhythmbox要好些,GNOME下Rhythmbox应该比Amarok要好些。

Amarok比Audacious功能上要强大,Audacious比较简洁,还是看个人选择。我比较喜欢Audacious,因为它比较简洁稳定,系统资源占用也比Amarok少。

音频编辑软件

工具

Audacity

Linux下最受欢迎的音频编辑软件。最类似于cooledit的linux音频处理软件,功能上也比较类似,适合于翻唱和后期处理,在截取、降噪、渐变改变音质等方面表现的相当专业。

Ardour

Ardour是一个数字音频工作站,它可不是给一般人用的。Ardour对于音乐家、工程师、原声带编辑人,和作曲家就如Audacity对于播主们——是最好的工作助手。

其它

FFmpeg

选择

Audacity > Ardour > FFmpeg

视频播放器

工具

VLC

VLC多媒体播放器(最初命名为VideoLAN客户端)是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影音光盘及各类流式协议。它也能作为unicast或 multicast的流式服务器在IPv4或 IPv6的高速网络连接下使用。它融合了FFmpeg计划的解码器与libdvdcss程序库使其有播放多媒体文件及加密DVD影碟的功能。

MPlayer

MPlayer基于命令行界面,在各操作系统也可选择安装不同的图形界面。

SMPlayer

SMPlayer是MPlayer的一个图形化前端,基于qt4库开发的。具有十分完备的功能,可以支持大部分的视频和音频文件。它支持音频轨道切换,允许调节亮度、对比度、色调、饱和度、伽玛值,按照倍速、4倍速等多种速度回放,还可以进行音频和字幕延迟调整以同步音频和字幕。

选择

Linux下的视频播放器前三绝对是它们三个了。

vlc ; MPlayer ; SMPlayer

其实对我来说VLC和SMPlayer都差不多,上面的优先级是按受欢迎程度来排的。

视频编辑

工具

Kdenlive

Kdenlive是一套开源的视频非线编辑软件。Kdenlive可以通过FFmpeg 编辑所有格式的视频文件,这就意味着DV、HDV、mpeg、avi、mp4、mov、flv、ogg、wav、mp3和vorbis这些格式都将被支持。Kdenlive是一款非常专业的视频编辑软件。可以毫不夸张的说Kdenlive是Linux下最好的视频编辑软件。

其它(按受欢迎程度顺序列出)

Blender

Avidemux

Openshot

Cinelerra

选择

kdenlive > Blender > Avidemux > openshot > Cinelerra

云存储

工具

Dropbox

Dropbox是一款非常好用的免费网络文件同步工具,是Dropbox公司运行的在线存储服务,通过云计算实现因特网上的文件同步,用户可以存储并共享文件和文件夹。Dropbox提供免费和收费服务,Dropbox的收费服务包括Dropbox Pro和Dropbox for Business。在不同操作系统下有客户端软件,并且有网页客户端。

ownCloud

不了解,这里不做介绍。

Google Drive

Google Drive是谷歌公司推出的一项在线云存储服务,通过这项服务,用户可以获得15GB的免费存储空间。同时,如果用户有更大的需求,则可以通过付费的方式获得更大的存储空间。

选择

Dropbox ; ownCloud ; Google Drive

对于国内Linux用户来说,百度网盘是一个不错的选择。目前仅有网页版本可用。

网志工具

工具

Hexo

Hexo是一款网志程序,基于Node.js的,功能非常强大,适合有技术的人才搭建网志。Hexo是快速、简洁且高效的网志框架。

CSDN网志

CSDN个人感觉国内做的最好的IT技术网站,现在越来越喜欢CSDN的网志功能,尤其是其支持Markdown格式,真的是技术控的福音。各位骚年,赶紧去试一试吧!

Wordpress

WordPress是一个免费的开源项目,在GNU通用公共许可证下授权发布。WordPress是一种使用PHP语言开发的网志平台,用户可以在支持PHP和MySQL数据库的服务器上架设属于自己的网站。也可以把 WordPress当作一个内容管理系统(CMS)来使用。

Jekyll

Jekyll是一个简单的免费的Blog生成工具,类似WordPress。但是和WordPress又有很大的不同,原因是jekyll只是一个生成静态网页的工具,不需要数据库支持。但是可以配合第三方服务,例如Disqus。最关键的是jekyll可以免费部署在Github上,而且可以绑定自己的域名。

Octopress

Octopress是一个基于Ruby的开源Blogging Framework,在我看来Octopress天生就是为技术控准备的Blog,因为从写blog,到发布,你完全可以用Shell里面的命令搞定。这样,写起Blog来,会让技术控们觉得很有成就感。

选择

Wordpress, Jekyll, Octopress用的人都挺多,都挺不错的。我没用过,具体的这里不做评论。介绍下我写网志的方式:Hexo + Github + CSDN网志。

我会在本地搭建Hexo网志框架,然后在该框架中用Markdown的文本格式写网志文档,然后通过Hexo生成静态页面,push到Github上,然后通过Github生成Github网志。最后再将该Markdown文件导入到CSDN网志。所以基本上很容易就实现一式三份:本地一份,CSDN网志一份,Github网志一份。

办公套件

工具

LibreOffice

LibreOffice是OpenOffice的一个分支,但功能要比OpenOffice多。LibreOffice是目前最好的办公套件。

OpenOffice

OpenOffice是一套跨平台的办公室软件套件,能在Windows、Linux、MacOS X (X11)和Solaris等操作系统上执行。

Google Docs

谷歌办公套件,类似于微软的Office的一套在线办公软件,可以处理和搜索文档、表格、幻灯片,并可以通过网络和它人分享,有google的帐号就能使用。使用感觉很不错,但缺点是在线的办公软件。

Koffice

KDE环境下的办公套件,比Libreoffice小巧。

Gnome Office

Gnome环境下的办公套件。

选择

LibreOffice > OpenOffice。

图像处理

工具

GIMP

GIMP是GNU图像处理程序(GNU Image Manipulation Program)的缩写。包括几乎所有图象处理所需的功能,号称Linux下的PhotoShop。

InkScape

Inkscape是开源的矢量图形编辑软件,与Illustrator、Freehand、CorelDraw、Xara X等软件很相似,它使用W3C标准的Scalable Vector Graphics (SVG)文件格式,支持包括形状、路径、文本、标记、克隆、alpha混合、变换、渐变、图案、组合等SVG特性。它也支持创作共用的元数据、节点编辑、图层、复杂的路径运算、位图描摹、文本绕路径、流动文本、直接编辑 XML等。它可以导入 JPEG、PNG、TIFF等格式,并输出为PNG和多种矢量格式。

Blender

Blender是一套三维绘图及渲染软件。有了Blender后,喜欢3D绘图的玩家们不用花大钱,也可以制作出自己喜爱的3D模型了。它不仅支持各种多边形画图,也能做出动画!倘若你觉得free版的不够使用,还能注册C-key,购买更强大的版本。Blender虽然是免费版本,不过它的功能可是又强又复杂。

Dia

Dia是开放源码的流程图软件,是GNU计划的一部分,程式创立者是Alexander Larsson。Dia使用single document interface (CSDI)模式,类似于GIMP。

Graphviz

Graphviz是大名鼎鼎的贝尔实验室的几位牛人开发的一个画图工具,它提供了“所想即所得”的理念,通过dot语言来编写脚本并绘制图形,简单易懂。感觉很酷!

其它

ImageMagick

yEd

选择

图形编辑: GIMP

3D作图: Blender

画流程图: Dia,想用编程的方式画就Graphviz

看图软件

工具

DigiKam

DigiKam是一款KDE桌面环境下的数字照片管理软件。非常专业。Linux下最受欢迎的照片管理软件。

Shotwell

Shotwell是一款GNOME桌面环境下的相片管理软件,适用于GNOME桌面环境。你可以使用它来从数码相机中导入相片,然后进行编辑并分享给朋友们。

F-spot

F-Spot是应用于GNOME的全功能的个人照片管理程序。利用F-Spot可以方便的从数码设备获取照片,并且可以创建属于自己的照片分类和电子相册,也可以上传到 Flickr,Google相册与朋友分享数码照片。

GwenView

是较好的一项应用,支持几乎所有图片格式,可进行基本的编辑、标签、缩略图、全屏、幻灯显示功能等等。

gThumb

gThumb 是一个GNOME桌面环境下的开源图像浏览器,遵循GPL版权协议。原先基于GQView,设计成为一个简洁的界面。

Eye of GNOME(eog)

是GNOME环境下较好的图片查看器,支持JPG,PNG,BMP,GIF,SVG,TGA,TIFF or XPM等图片格式,也可放大、幻灯显示图片、全屏、缩略图等功能。

display

Linux都默认安装的,非常原始的在X Window上展示图片的命令行工具。

选择

如果是想作为一个照片管理器用:DigiKam > Shotwell > F-spot > GwenView > gThumb

如果仅仅想查看一个图片: Shotwell > GwenView > eog > display, 如果在GNOME环境下GwenView要好于Shotwell。eog占用资源要比GwenView少,启动要比GwenView快。

当然还有其它的图片浏览工具,上面的是Linux下比较常用的。

科学制图

工具

Gnuplot

Gnuplot是一个比较强大的绘图软件包,可以进行绝大多数的科学绘图。

QtiPlot

完全成熟的绘图软件。从功能上讲,QtiPlot与windows下的origin几乎是一样的,连界面设计也极其相同,因此网上有人称它为 an open origin。这个在Linux上如果要用免费版的需要手动编译。

Metapost

一种画图语言,可以精确的画出你想要的图形。常与Latex配合使用。

Asymptote

与metapost相似但更易用的类C语言。

Geogebra

非常直观的几何作图软件。

MayaVi

MayaVi 在梵语中的意思是魔术师,它是一种数据可视化工具。

选择

这些绘图工具我只用过Gnuplot, 如果是比较简单的绘图Gnuplot就适合你。如果是比较专业的绘图Metapost,Asymptote,QtiPlot会有一款能满足你的。

参考

Linux下的绘图和图形处理

科学计算

工具

Octave

一种高级语言,主要设计用来进行数值计算,多数语法与matlab兼容,qtoctave是它的一个与matlab相似的前端。

Scilab

诞生于1994年,由法国的INRIA和ENPC设计。

PSPP

GNU用以取代SPSS的统计软件。

Qalculate

Qalculate是一个功能超级强大的计算器。它具有多种用途,不仅可以用于一般的计算工作,而且对于函数、单位、各种精度、制图等的计算同样能够胜任。当前,Qalculate包括命令行端的程序、GTK+界面的程序、以及KDE界面的程序。感觉使用上并没有什么门槛,但要熟悉的话,还是需花一定的时间去琢磨。

Galculator

galculator是一个基于GTK2/GTK的代数模式、RPN和公式的输入模式3为基础的科学计算器。功能包括算术运算,加上优先处理,全键盘的支持,三角函数,乘积,平方根,自然和常用对数,常数(E,PI),和反双曲函数。它支持不同的数字进制(十进制,十六进制,八进制,二进制)和角度基地(radiant, degree, 和grad)。

SpeedCrunch

SpeedCrunch 是一款强大的高精度桌面计算器,支持包括Windows、Linux和MacOS系统。

Kcalc

Kcalc这个工具更像你的标准计算器,捎带一点点附加功能。Kcalc提供了一个更加标准的界面),因此各式各样的人们都能够很容易地使用这款计算器。Kcalc是个非常轻量级的图形界面的计算器,如果你只想做一些简单的计算,那么可以考虑它。

bc

bc是一款字符界面的计算器,所有Linux发行版都会默认安装的任意精度的计算器。能满足大部分科学计算,性能高,使用方便。Linux下我最常用的计算器。

Awk

Awk本是专门用来处理文本的, 但它同时提供了一些基础的数值函数, 如:

atan2(y, x) 返回y/x的正切值;

int(x) 返回x的整数部分;

srand(x) 设置虚拟随机产生器的种子;

rand() 返回平均分布的虚拟随机数r, 0<=r<1;

sin(x), cos(x), exp(x), log(x), sqrt(x).

Awk支持标量变量, 数组变量, 赋值, 算数运算, 逻辑运算, 函数和控制结构, 可构造复杂的运算过程.

expr

expr命令可不光能计算加减乘除哦,还有很多表达式,都可以计算出结果,不过有一点需要注意,在计算加减乘除时,不要忘了使用空格和转义。

dc

用dc来进行计算的人可以不多,因为dc与bc相比要复杂,但是在进行简单的计划时,是差不多的,不算难。dc为压栈操作,默认也是交互的,但也可以用echo和|来配合打算。

echo

echo用来进行回显,是周知的事。上面也配合bc来进行计算。其实echo也可以单独进行简单的计算,如:

`# echo $((3+5)) 8 # echo $(((3+5)*2)) 16` * 1 * 2 * 3 * 4
选择

如果你要做专业的科学计算那么Octave是最好的选择,Scilab可以作为第二选择。

如果你只是想在图形界面下进行稍微复杂点的计算,Galculator是我的第一选择,Qalculate我会把它作为第二选择。SpeedCrunch没用过这里不做评论。至于Kcalc类似于Windows附件中的计算器,非常的轻量级,当然功能也有限。

字符界面下我一般用bc,因为它很方便。dc几乎没用过,但功能应该跟bc差不多。至于awk和expr我会在编写shell脚本时有选择的使用。比如在一个awk程序块中当然是用awk来进行计算了。

虚拟机

工具

VirtualBox

VirtualBox是一款功能强大的x86虚拟机软件,它不仅具有丰富的特色,而且性能也很优异。

VMware

VMware不是开源软件。VMware公司是全球著名的虚拟机软件公司,目前为EMC公司的全资子公司。

在Linux下可用的VMware虚拟化产品为:

VMware Workstation是vmware面向桌面的主打产品。与VMware Server不同,VMware Workstation专门针对桌面应用做了优化,如为虚拟机分配USB设备,为虚拟机显卡进行3D加速等。VMware Workstation是收费的。

VMware Player是简化版的Workstation,是免费版的。

KVM

KVM是一款开源的虚拟机管理软件,性能优异,稳定性好。在Fedora上安装非常方便,只需要打开BIOS的虚拟化开关,安装用户空间模拟器qemu-kvm即可。KVM有众多的命令,对于刚接触KVM的用户来说,可能会不太好上手。但Fedora已经提供了virt-manager,virt-viewer,virt-install等图形界面的管理工具。使用起来还是很方便的。

Xen

Xen是一款非常成熟的开源虚拟机管理软件,是类虚拟化的典型代表,但Xen安装起来相对麻烦一些,在Fedora没有图形界面的管理工具,只能通过xl/virsh命令行工具来管理虚拟机,所以不推荐使用。

QEMU

QEMU这是比Xen更老的模拟器,功能有限,操作麻烦,不推荐使用。

Citrix XenServer

这个就更不推荐了,因为XenServer是Xen + CentOS5的结合体。你不可能在你的Fedora或Ubuntu上安装XenServer。

选择

如果你想要快速的在一个图形界面的管理器上创建虚拟机,那么VirtualBox是你的首选,KVM作为你的第二选择,VMware Player作为你的第三选择。

如果你喜欢在命令行下操纵你的虚拟机,那么KVM是首选,Xen作为你的第二选择。

监控应用

工具

Nagios

Nagios是一个监视系统运行状态和网络信息的监视系统。Nagios能监视所指定的本地或远程主机以及服务,同时提供异常通知功能等。Nagios本身并不包含任何监控机制,其所有的监控工作都是通过插件(plugin)来实现的。

OpenNMS

OpenNMS是一个企业级基于Java/XML的分布式网络和系统监控管理平台。OpenNMS是你管理网络的绝好工具,它能够显示你网络中各中终端和服务器的状态和配置,为你方便地管理网络提供有效的信息。

Zabbix

Zabbix是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案。

Zabbix能监视各种网络参数,保证服务器系统的安全运营;并提供柔软的通知机制以让系统管理员快速定位/解决存在的各种问题。

Wireshark

Wireshark(前称Ethereal)是一个网络封包分析软件。网络封包分析软件的功能是撷取网络封包,并尽可能显示出最为详细的网络封包资料。

Wireshark不是入侵侦测软件(Intrusion DetectionSoftware,IDS)。对于网络上的异常流量行为,Wireshark不会产生警示或是任何提示。然而,仔细分析Wireshark撷取的封包能够帮助使用者对于网络行为有更清楚的了解。Wireshark不会对网络封包产生内容的修改,它只会反映出目前流通的封包资讯。 Wireshark本身也不会送出封包至网络上。

Zenoss

Zenoss Core是开源企业级IT管理软件-是智能监控软件,它允许IT管理员依靠单一的WEB控制台来监控网络架构的状态和健康度。Zenoss Core同时也是开源的网络与系统管理软件。

htop

htop 是一个Linux下的交互式的进程浏览器,可以用来替换Linux下的top命令。

atop

atop是一个用来查看Linux系统负载的交互式监控工具。它能展现系统层级的关键硬件资源(从性能角度)的使用情况,如CPU、内存、硬盘和网络。

top

经典的Linux下的监控命令。用过Linux的都知道这个命令。

其它

Icinga

ICINGA项目是 由Michael Luebben、HendrikB?cker和JoergLinge等人发起的,他们都是现有的Nagios项目社区委员会的成员,他们承诺,新的开源项 目将完全兼容以前的Nagios应用程序及扩展功能。在新项目的网站上,他们是如此定义ICINGA的,这将是一个介于Nagios社区版和企业版间的产 品。特别将致力于解决Nagios项目现在的问题,比如不能及时处理Nagios项目的bug、新功能不能及时添加等。还有在新的ICINGA项目中,将 更好的实现数据库集成方面的功能,标准化第三发应用程序的接口等。期待中。

选择

监控系统和网络: Nagios > OpenNMS > Zabbix > Wireshark > Zenoss

命令行监控工具: htop,atop,top都不错,可以根据自己的习惯进行选择。

编程IDE

工具

Eclipse

Eclipse是著名的跨平台的自由集成开发环境(IDE)。最初主要用来进行Java语言开发,但是目前亦有人通过插件使其作为其它计算机语言比如C++和Python的开发工具。

VIM

Linux下非常好用的编辑器,配置型强,可以配置为自己喜欢的IDE。VIM控的首选。

Emacs

Emacs是比肩VIM的又一款编辑器,它也有非常强的配置性,也可以配置为自己喜欢的IDE,Emacs控的首选。

Kdevelop

KDE下集成开发环境,支持多种程序设计语言。

选择

图形界面IDE: Eclipse

VIM控: VIM

Emacs控: Emacs

运维配置管理工具

工具

Puppet

Puppet是一种Linux、Unix、Windows平台的集中配置管理系统,使用自有的Puppet描述语言,可管理配置文件、用户、cron任务、软件包、系统服务等。Puppet把这些系统实体称之为资源,Puppet的设计目标是简化对这些资源的管理以及妥善处理资源间的依赖关系。

Ansible

Ansible提供一种最简单的方式用于发布、管理和编排计算机系统的工具。

Foreman

Foreman是一个集成的数据中心生命周期管理工具,提供了服务开通,配置管理以及报告 功能,和Puppet Dahboard一样,Foreman也是一个Ruby on Rails程序。Foreman和 Dashboard不同的地方是在于,Foreman更多的关注服务开通和管理数据中心的能力,例如和引导工具,PXE启动服务器,DHCP服务器及服务器开通工具进行集成。

Foreman 机器统一管理平台:

Foreman可以与Puppet集成使用,通常是作为puppet的前端接入。

Foreman takes care of provisioning until the point puppet is running, allowing Puppet to do what it does best.

Foreman能够通过Facter组件显示系统目录信息,并且可以从Puppet主机报表中提供实时信息。

Foreman能够准备你管理新机器的所有工作。它的设计目标是能够自动化的完成所有手工管理的工作,通过Foreman可以重新配置机器。

Foreman能够管理大规模(当然也包括小规模)的,企业级的的网络,可能有很多域,子网和很多puppet master节点。Foreman也可以实现配置版本的回溯。

其它

Cron jobs

Subversion

Chef

SaltStack

CFEngine

NixOps

选择

Puppet(Puppet + Foreman)> Ansible

如果需求比较简单就:Ansible > Puppet(Puppet + Foreman)

桌面环境

工具

KDE

KDE,K桌面环境(Kool Desktop Environment)的缩写。一种著名的运行于 Linux、Unix 以及FreeBSD等操作系统上面自由图形工作环境,整个系统采用的都是TrollTech公司所开发的Qt程序库。KDE和Gnome都是Linux操作系统上最流行的桌面环境系统。

GNOME

GNOME是一种支持多种平台的开发&桌面环境,可以运行在包括GNU/Linux(通常叫做Linux),Solaris,HP-UX,BSD和Apple’s Darwin系统上。GNOME拥有很多强大的特性, 如:高质量的平滑文本渲染,首个国际化和可用性支持,并且包括对反向文本的支持(注:有些国家的文字是从右到左的排版的)。

XFCE

Xfce是一款适用于多种Linux系统的轻量级桌面环境。它被设计用来提高您的效率,在节省系统资源的同时,能够快速加载和执行应用程序。

LXDE

LXDE专案旨在提供一个新的轻量、快速的桌面环境。相较于功能强大与伴随而来的膨胀性,LXDE注重于实用性和轻巧性,并且尽力降低其所耗系统资源。不同于其它桌面环境,其元件相依性极少。取而代之的是各元件可以独立运作,大多数的元件都不须倚赖其它套件而可以独自执行。

Fluxbox

Fluxbox是一个基于GNU/Linux的轻量级图形操作界面,它虽然没有GNOME和KDE那样精美,但由于它的运行对系统资源和配置要求极低,所以它被安装到很多较旧的或是对性能要求较高的机器上,其菜单和相关配置被保存于用户根目录下的.fluxbox目录里,这样使得它的配置极为便利。

Fvwm

FVWM作为一种虚拟桌面的代表,宗旨为以最小的内存换取最多的特性。FVWM可以轻而易举的模拟大多数的桌面系统和自定义的桌面。

FVWM的优势:

FVWM启动/重启速度很快;

FVWM界面很漂亮,可以说FVWM的截图是最值得欣赏的,而且虚屏功能是所有WM中最强大的;

FVWM占用内存很少,与TWM相比,相差不到1M(用free查看),但是界面可以很酷;

可以对多种系统统一桌面,使各种系统桌面一致,并且部署容易,简单的配置文件直接拷贝就行;

FVWM可以把桌面发挥到极限:(256M内存)曾经同时跑6个 Bochs系统(虚拟机),openoffice,mozilla,gthumb,gaim等,窗口反应迅速,虚拟桌面有10*4个,依然切换迅捷,整个桌面看起来依然简洁。而且调整FVWM配置,重启FVWM很多次,从来没有崩溃过;

FVWM简单的通过配置实现桌面的新功能,例如:自动伸缩的邮件通知等等;

最有潜力的扩展方式,用perl语言可以快速的扩展FVWM的功能;

选择

桌面环境的选择,完全由自己的喜好决定,适合你的就是最好的。下面是我使用KDE,GNOME,FVWM后的感受。XFCE、LXDE因为没用过,这里不做评价。

KDE是我目前所使用的桌面环境,KDE给我的最大感觉是定制性强,尤其是快捷键。KDE的定制性要比GNOME强。KDE功能强大,里面集成了不少非常优秀的软件,尽管如此KDE也是非常的稳定。

GNOME3给我的感觉是很前卫,界面很漂亮。GNOME3里有很多新颖的设计和非常不错的软件。但是因为不太喜欢GNOME3的设计,所以在2013年的时候转投KDE,从那时起便喜欢上了KDE。

FVWM给我的感觉是一切你都需要自己定制: 桌面、任务栏、鼠标左右键选项、定义各种行为等等。你可以把桌面配置成任何你想要的形式。总之FVWM非常灵活,跟KDE,GNOME,XFCE等等感觉完全不一样。建议大家尝试一下。FVWM有很多优势(上面有列举),也有自己的缺点比如配置量大,功能没有KDE、GNOME和XFCE功能强大等。但对追求简约的Linux用户来说还是一款值的一试的桌面环境。

输入法

fcitx: 小企鹅输入法,国产

scim: GTK输入法

ibus: Linux下的智能输入法,可与搜狗拼音相媲美

fcitx是我目前使用的输入法框架,在这个输入法框架中我会装上sunpinyin输入法,当然现在我用的是搜狗输入法,感觉搜狗输入法真的非常棒,应该说是最好的中文输入法了。fcitx对五笔的支持也非常好。

chm阅读器

工具

kchmviewer

它是KDE下的chm查看器。对中文支持很好,KDE环境下第一选择。

chmsee

ChmSee是一款非常出色的CHM阅读器,小巧轻便,兼容性也很出色。

Gnochm

Gnochm功能和界面都跟windows下的chm阅读器基本一样,没有乱码。

Xchm

这是由外国程序员开发的一款CHM阅读器,优点是和win下的CHM阅读几乎一模一样,呵呵,这个如果在win看习惯了,比较容易接受,查看英文的chm文件的时候,效果非常漂亮。

选择

这些chm阅读器最大的不同在于对中文的支持,选择一款对中文支持好的就可以了。kchmviewer是我目前正在用的软件对中文有很好的支持。推荐给大家。Gnochm也非常不错。

思维导图软件

FreeMind

FreeMind是一款简单易用的思维导图软件,可以帮助我们快速地绘制出思维导图,帮助我们快速有序地组织思路。

XMind

XMind界面美观,兼容FreeMind和MindManager等流行思维导图软件的数据格式,而且功能丰富,不仅可以绘制思维导图,还能绘制鱼骨图、二维图、树形图、逻辑图、组织结构图,是一款非常出色的的思维导图和头脑风暴软件。

SlideShow

工具

做Slides的方法有很多,每个人的选择会不同,下面是我曾经用来做Slides的工具,仅供参考。

LibreOffice Impress

类似于Windows下的PowerPoint, 是做幻灯片的不错选择,如果不喜欢倒腾的话。

Beamer

Beamer is a LaTeX package for writing presentations.

reveal.js

reveal.js is an Org-mode extension that exports Org documents into Reveal.js presentations. Reveal.js is a web-based presentation framework with 3-D effects, customizable themes and animations, powered by the latest HTML5 technologies.

S5

S5 is a standards-based suite for writing slide-show presentations in html web pages. The browser is used as the presentation engine, and a slightly altered form of Org-mode’s HTML export serves as the base of the presentation.

org-html5presentation

org-html5presentation is an Exporter of Org-mode documents to HTML5 slide show presentations.

tpp

命令行模式下的幻灯片展示工具 tpp - the command line presentation tool

参考

Tools for Creating Screen or Online Presentations

Writing Beamer presentations in org-mode

Writing Non-Beamer presentations in org-mode

选择

我用的是Beamer,效果大家可以下载emacs介绍(PDF幻灯片)查看。

时间管理工具(GTD)

工具

TaskCoach

Task Coach是一款开源的个人事务管理工具,并且主要针对个人的代办事项。这个软件不同于同类型的软件如Outlook或者Lotus Notes等,因为这些软件都不具备合成事务的功能。因为一般来说,一件事务的办理都有几个相关步骤的,而Task Coach正是从这个特性出发而设计的,尤其适合对复杂事件的处理。目前,Task Coach包括创建任务及子任务、设置任务类别、跟踪任务的完成进度、添加任务笔记、打印或输出任务、通知提醒等功能。使用Task Coach这个简单的Todo管理器,相信能让你的工作更加井井有条。

Calcurse

Calcurse是一个基于文本界面的个人日程安排软件,可对事件、委任和每日事务进行跟踪,可配置的提醒系统。

Taskwarrior

Taskwarrior是一个基于命令行的TODO列表管理工具。主要功能包括:标签、彩色表格输出、报表和图形、大量的命令、底层API、多用户文件锁等功能。

emacs

毫无疑问你可以把Emacs配置成一个GTD工具, 只要你愿意。在Emacs下很容易配置的。

Tasque

Tasque是一个Linux下简单的图形化任务管理工具。

Yokadi

命令行任务管理系统。

选择

想用比较专业的图形界面的任务管理器: TaskCoach。我用过一段时间,但老崩溃,不过TaskCoach目前正处于积极的开发阶段,相信它会越来越稳定的。

想用基于ncurses的带界面的任务管理器: Calcurse

想用命令行下的任务管理器: Taskwarrior

Emacs控: 那就用emacs自己配置出一个GTD吧

推荐: TaskCoach

PDF编辑软件

工具

Pdftk

如果PDF是一张电子纸,Pdftk就是一个印戳涂抹器、打孔机、浆糊、显影液、和一个X光玻璃。Pdftk是一个简单的PDF万用工具,使用它,你可以:

合并PDF文档

分割PDF

旋转PDF页面

解密PDF密码

加密PDF

使用FDF Data或者XFDF来填写PDF窗体

添加水印或者标签

显示PDF信息

修改PDF信息

附加文件到PDF页面或者PDF文档

解压PDF附件

分解PDF文档成单页形式

解压和重新压缩PDF流

修复受损的PDF文档

Pdftk让你轻松管理你的PDF文档,并且是免费的,可以在Windows、Linux、Mac OS X、FreeBSD和Solaris。

Pdfchain

Pdftk的GUI工具。

Pdfedit

PDFedit可以让你整个的编辑PDF文档。你可以改变PDF的任意部分。功能可以使用脚本添加。脚本可以使用其它外部编辑器,并且可以定制自己的脚本。

PdfMod

PDF Mod是一个简单的PDF修改工具。你可以调整页的顺序、删除页面、导出文档里面的图像,编辑标题、主题、作者和关键词并且可以通过拖拽来合并文档。

PDF-Shuffler

PDF-Shuffler是一个使用python-gtk写成的小工具,它可以协助使用者合并或分割PDF文档,另外也可以对PDF的每一页做旋转、切割或重新排序。事实上它就是python-pyPdf的一个图形化使用者界面。

Xournal

Xournal是一个用于书写备忘笔记、草图的编辑工具。但它有一个特色功能,就是可以导入及导出PDF文件,所以我们也可以把它当作PDF批注工具,当你拿到一个PDF文件后,你可以用此工具导入PDF文件,并可对局部内容进行高亮、文字批注等操作,导出后再分发给其它人,非常简单易用。

其它

PDFsam

选择

尝试了Linux下的各种PDF编辑工具,若只是做一些简单的PDF页面分割与合并功能,感觉最好用的就是PDF Mod,其界面做的比较棒,但若是要处理比较大量的PDF文档或是更复杂的文字编辑,那就要考虑PDFedit或是其它几个工具了。

性能测试工具

工具

CPU nbench, linpack, SPECjbb2015

内存 LMbench, stream

网络IO netperf (最专业的网络IO benchmark工具,应该是Linux下用的最多的), iperf

磁盘IO dd, iozone, bonnie++, dbench, fio(推荐), smallfile

Mysql sysbench, httperf

HTTP ab, httperf, webbench

Java SPECjvm2008

开源测试套件 ltp

参考

Linux性能测试之基准测试工具

Performance Testing

Linux Benchmark Tools

压力测试工具

CPU stress

内存 stress

磁盘IO iozone, bonnie++

网络IO netperf

Linux终端

工具

Yakuake

KDE下的下拉式终端,也是我最喜欢的。稳定,配置性高,功能全。

Guake

GNOME下的下拉式终端,也是一款非常不错的终端,唯一的缺点是不支持alt+number键切换TAB。不过可以通过修改代码的方式解决。但是,即使支持了alt + number键切换TAB,还有个问题没法解决就是底部的TAB没法隐藏。总的来说很喜欢这款终端。

Tilda

又一款下拉式终端。跟Yakuake一样非常棒。它的快捷键和功能没有Yakuake,但是作为一款终端软件现有的功能已经完全够用了。

Terminator

非下拉式终端中功能最全,最完美的一款终端。

Stjerm

这是一款可以和Guake、Tilda相媲美的终端软件。而且它非常的轻巧,有tab页,可以全屏切换。

选择

我现在基本不用非下拉式终端,在日常的工作学习中我通常会Yakuake,Guake,Tilda一起用。Yakuake用来工作,Tilda用来学习,Guake用来它用。

在非下拉式终端中我会选择: Terminator, Terminator配置性强,自带分屏功能。

参考

12款最佳Linux命令行终端工具

SQL数据库

工具

MySQL

MySQL是开源数据库中的佼佼者,它的用户数是在同类开源数据库中是最多的,它既可以被小的新兴公司所使用,也可以被采用了操作系统集群的大型Web站点所使用。

PostgreSQL

在开源数据库中,PostgreSQL以其丰富的功能而显得格外突出,其中包括存储过程、表分区(partitioning)、多过程语言支持和多种数据类型和索引的支持。

SQLite

SQLite是一个轻量级、跨平台、容错性强、数据便于迁移的关系型数据库。

MariaDB

MariaDB是一个采用Aria存储引擎的MySQL分支版本,是由原来MySQL的作者Michael Widenius创办的公司所开发的免费开源的数据库服务器。

Oracle

Oracle是商业数据库的代表,具有非常丰富的功能、广泛的平台支持和大量的附加功能。

其它

DB2

选择

如果想要一个功能强大,稳定的数据库: MariaDB/MySQL

如果想要一个轻量级,性能稳定,便于迁移的数据库:SQLite

PostgreSQL没用过。

NoSQL数据库

工具

MongoDB

MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

CouchDB

Apache CouchDB是一个面向文档的数据库管理系统。它提供以JSON作为数据格式的REST接口来对其进行操作,并可以通过视图来操纵文档的组织和呈现。

Cassandra

Apache Cassandra是一套开源分布式Key-Value存储系统。

Redis

Redis是一个高性能的key-value数据库。 Redis的出现,很大程度补偿了memcached这类keyvalue存储的不足,在部分场合可以对关系数据库起到很好的补充作用。

选择

这些我都没用过NoSQL数据库我都没用过。

备份软件

工具

rsync

轻量级的备份工具,能应付大部分情况。

tar

Linux tar命令。

Amanda

Amanda是一个备份系统,允许管理员建立一个单一的主备份服务器备份多台主机的网络,磁带机/兑换或磁盘或光学介质。

Bacula

Bacula是一套计算机程序,允许系统管理员来管理备份,恢复和核查在网络上的计算机数据。 它可以备份到不同类型的媒体,包括磁带和磁盘。在技术方面,它是一个网络客户机/服务器的备份程序。相对易于使用和高效,同时提供许多先进的存储管理功 能,可以很容易地找到和恢复丢失或损坏的文件。由于它采用模块化设计,具有很好的伸缩性。

选择

不复杂的备份任务: rsync

复杂的备份任务: Bacula/Amanda

游戏

Linux下的游戏比较多, 这里就推荐一款非常非常经典的游戏。不得不说图形显示是Linux下游戏的硬伤。

NetHack

NetHack(Wiki),20年历史的古老电脑游戏。没有声音,没有漂亮的界面,不过这个游戏真的很有意思。网上有个家伙说:如果你一生只做一件事情,那么玩NetHack。

其它实用工具

远程桌面客户端 rdesktop

屏幕管理器 screen, tmux(与screen类似,可作为screen的替代品)

监控文档 tail, multitail

TTY录制工具 ttyrec && ttyplay (与此类似的还有Shelr和termrec)

截图工具 shuttle, scrot

密码生成器 pwgen, mkpasswd, makepasswd

密码管理工具 keepassX, keepass2 (我用的是keeppassX)

版本管理工具 git, gitg, gitlab(gitg是git的图形界面,gitLab是一个用于仓库管理系统的开源项目,类似于github), svn (Linux下推荐用git)

财务管理 gnucash, homebank, ledger (gnucash更专业些,我用的是这个)

密码破解 John the Ripper, Hydra, Medusa, Ophcrack (ophcrack是图形界面的工具)

入侵检测 PSAD, Snort, Tripwire, chkrootkit和rootkit。

笔记软件 CherryTree, Zim,印象笔记(Web版),为之笔记 (目前我用的是CherryTree)

数据删除 shred(Linux coreutils), wipe, srm, bleachbit(GUI工具)。此外还有一些可以删除内存和swap中内容的工具。参考使用 Linux 安全删除工具

歌词软件 osdlyrics (用了2年了,非常稳定,功能很多)

X窗口交互工具 wmctrl

粘贴板工具 xclip

在线Markdown工具 Cmd Markdown 简明语法手册

省电工具 powertop

硬盘IO监视工具 iotop

管道查看器 pv

语音合成器 espeak

虚拟天文馆 stellarium

家谱程序 gramps

正则表达式的开发和执行工具 redet

文件去重工具 fdupes

彩色man界面 most

监控系统温度、电压等 lm-sensors

家庭影院 xbmc

文件夹比较和同步工具 freefilesync

终端控制数字键盘开启关 numlockx

开启笔记本触摸板 synclient TouchPadOff=0

关闭笔记本触摸板 synclient TouchPadOff=1

代码统计工具 cloc

查看系统性能 dstat (iostat, vmstat, ifstat三合一的工具)

从标准输入读取数据并执行 xargs, parallel

压缩/解压工具 rar, unrar, zip, tar, gzip, bzip2等

Linux下的电驴 amule

系统优化工具 tweak (注意:每个桌面环境都有自己不同的tweak工具)

视频聊天 Skype

终端交互过程录制 ttygif

记录终端会话 script

终端里的记录器 script,记录某人在终端中的所作所为:

` $ vi ~/.profile # run the script command to record everything # use -q for quite and -a option to append the script # /usr/bin/script -qa /usr/local/script/log_record_script` * 1 * 2 * 3 * 4 * 5 * 6
简历模板 moderncv (mcv, moderncv 的笔记)

其它工具(比较有趣)

cowsay Cowsay命令是一个有趣的命令。它会用ASCII字符描绘牛,羊和许多其它动物,并让它们说出你想输出的话。

xcowsay Cowsay的X Window版本,会在屏幕上显示一小奶牛。

cowthink 牛在思考,而不是说话。

aafire 在你的终端放一把火。

asciiquarium 在终端弄一个水族馆。

banner 在终端用很大的字符打印你输入的字符串。

echo “Tecmintcom is a community of Linux Nerds and Geeks”|pv -qL 10 匀速打字。

asciiview 在屏幕上用ASCII码格式显示一张图片。

sl 一辆火车呼啸而过。

yes 重复输出字符串直到被杀死

xeyes 一双萌萌的眼睛一直盯着你。

toilet 将输出的文本添加边框。

rev 它会把传递给它的的每个字符串都反过来

oneko 一只猫在追老鼠(鼠标指示器)。

linuxlogo 查看当前比较流行的Linux发行版的Logo。

fortune 随机输出一条谚语或信息。

figlet 用大写方式把我们输入的字符串显示在终端,显示效果由ASCII字符组成。

cmatrix 这个命令会在终端生成ASCII字符组成的矩阵风格的动画效果。

结语

为了给大家分享这些,我花了很大的功夫来整理Linux下的软件,Linux下好用的软件超多,肯定还有很多优秀的软件这篇文章没有涉及到,如果大家知道其它好用的软件/工具,还希望能够发扬开源精神分享出来,或者以留言的形式,或者直接发email给我,我会更新到这篇文章中。希望这篇文章能够对刚接触Linux的朋友有所帮助。因为这篇文章借鉴了很多网上整理的资料,所以这里要感谢一下整理这些资料的作者。

[ Last edited by zzz19760225 on 2016-2-3 at 20:29 ]



1<词>,2[句],3/段\,4{节},5(章)。
2016-2-3 20:27
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 14 楼』:  13BASIC初学者通用的符号指令代码 14楼

freebasic
编辑词条
该词条缺少摘要、基本信息栏、词条分类,补充相关内容帮助词条更加完善!立刻编辑>>


1属性
2特点
3不足点
1 属性 编辑
【语言】FreeBASIC,BASIC 语言界的黑马

学习过 QuickBASIC 的用户就可以上手的 BASIC 语言,

完全免费开源,能够产生高品质的机器码,跨平台,

FreeBASIC如同他的名字一样,免费而且基于已建立的BASIC语法,

2 特点 编辑
易学易用是他的优点,但是不光是简单而已,功能十分强大。

- 几乎支持所有QB的原指令,且有许多追加功能

- 产生快速高品质的机器码,不依靠VM等虚拟机器

- 完全免费,包含源代码,编译出来的程序无授权问题

- 支持MS-DOS/Win32/Linux多平台,也可以编译GUI程序

- 拥有众多第三方函数库支持(Allegro/SDL..以及DirectX/Win32API)

- 支持Unicode,使用中文十分容易

- 编译EXE/OBJ/LIB/DLL都很容易,以便和其他语言应用

3 不足点 编辑
- 代码最佳化还没有100%完成

官方主页http://www.freebasic.net/

FBEdithttp://fbedit.freebasic.net/

FBIdehttp://fbide.freebasic.net

FreeBASIC是一个免费自由的 32位BASIC编译器.可以运行在Windows(32位),保护模式的 DOS 和linux(x86)系统之上。它最初是作为一个代码兼容,自由免费的 微软QuickBASIC的替代品而开发的,但是现在已迅速成长为一个强大的开发工具。默认安装已经包含以下的函数库:Allegro, SDL, OpenGL, Gtk, Windows API 等等。

除了语法上最大程度上兼容微软QuickBASIC以外,FreeBASIC加入了一些新的特性,比如指针,无符号 数据类型, 内联汇编,预处理器等等。

FreeBASIC 是一个 self-hosting 编译器,(它的编译器是用 FreeBASIC 语言写的),由V1ctor开发。
------------------------------------------------------------------------
BASIC
编辑词条
该词条缺少基本信息栏、词条分类,补充相关内容帮助词条更加完善!立刻编辑>>

BASIC(Beginners' All-purpose Symbolic Instruction Code,又译培基),意思就是“初学者通用符号指令代码”,是一种设计给初学者使用的程序设计语言。在微电脑方面,则因为BASIC语言可配合微电脑操作功能的充分发挥,使得BASIC早已成为微电脑的主要语言之一。


1简介
2历史
初期
微机
结构化
Visual
3名字
4关于
5基本命令
6语言特点
1 简介 编辑
Beginner's All-purpose Symbolic Instruction Code(初学者通用符号指令代码),刚开始被作者写做 BASIC,后来被微软广泛地叫做 Basic 。BASIC语言是由Dartmouth学院JohnG.Kemeny与ThomasE.Kurtz两位教授于1960年代中期所创。由于立意甚佳,BASIC语言简单、易学的基本特性,很快地就普遍流行起来,几乎所有小型、微型以家用电脑,甚至部分大型电脑,都有提供使用者以此种语言撰写程式。在微电脑方面,则因为BASIC语言可配合微电脑操作功能的充分发挥,使得BASIC早已成为微电脑的主要语言之一。

随着计算机科学技术的迅速发展,特别是微型计算机的广泛使用,计算机厂商不断地在原有的BASIC基础上进行功能扩充,出现了多种BASIC版本,例如TRS-80 BASIC、Apple BASIC、GWBASIC、IBM BASIC(即BASICA)、True BASIC。此时BASIC已经由初期小型、简单的学习语言发展成为功能丰富的使用语言。它的许多功能已经能与其他优秀的计算机高级语言相媲美,而且有的功能(如绘图)甚至超过其他语言。

一般人类自然语言有标准语言,也有方言,电脑语言亦是如此。许多种电脑都有BASIC语言,但其语法、规则、功能却不尽然相同,而同一种电脑所使用的BASIC语言也可能有不同版本或由不同的软件开发公司制作的不同品牌BASIC语言,只是大家一致地继承了BASIC创始者所设计的基本形态与精神,而分别赋予独特的设计手法与增添一些功能罢了。

2 历史 编辑
初期
语言功能很弱、语句很少,只有14条语句,后来发展到17条语句,这就是所谓的“基本的BASIC”。这个时期的BASIC语言主要在小型机上使用,以编译方式执行。

微机
20世纪70年代,BASIC发展成为一种广泛使用的通用语言。也正是这个年代,微型计算机诞生了。第一个微型计算机配置BASIC语言的是微软(Microsoft)公司总裁比尔.盖茨,那时他才19岁。在比尔.盖茨的第一个微型计算机BASIC的带动下,各种计算机都相继配备了BASIC语言,由于机型不同,它们对基本BASIC语言的扩展也不相同,导致了同是BASIC语言程序却不能互相兼容的局面。这个时期的BASIC语言开始采用解释执行方式,方便了用户对程序的维护。

结构化
结构化程序设计思想是20世纪70年代开始萌发的,其主要思想是尽量使程序按传统书写顺序执行,减少语句之间的跳转,采用模块化设计,各模块完成一守的相对简单的功能。结构化程序能增加程序的可读性。

20世纪80年代中期,美国国家标准化协会(ANSI)根据结构化程序设计的思想,提出了一个新的BASIC标准草案。在此之后,出现了一此结构化的 BASIC语言,主要有Quick BASIC True BASIC等。它们不仅完全适应结构化、模块化的程序设计的要求,而且保留了BASIC语言易学、易用、易维护等优点,同时提供了解释执行方式和编译执行方式。

Visual
20世纪80年代中期,微软公司推出Windows操作系统,它提供了图形方式的用户界面,通过鼠标、窗口中、菜单等控计算机,使操作变得更直观、更简单,使用计算机更容易,更方便。

基于Windows操作系统的BASIC语言是Visual BASIC(意为“可视的BASIC”),由美国微软公司开发,它是微软公司在1991年推出的,是一种强有力的软件开发工具,应用它可以设计出具有良好用户界面的应用程序。Visual BASIC一出现就受到高度重视,发展潜力具大,比尔.盖茨宣称:“Visual BASIC是迎接计算机程序设计挑战的最好例子。”

1975 年,比尔·盖茨创立的 Microsoft,并成功的把 Basic 语言的编译器移植到使用 Intel处理器的 ALR 计算机中,IBM 在 1982 年选定 Microsoft 创作 PC 的操作系统时,也选定了 Microsoft 的 Basic 作为其计算机的 ROM-Basic。微软还在其发布的 DOS操作系统中免费加入了 GW-Basic、QBasic 等当时最好的 Basic解释程序。  Quick BASIC是微软(Microsoft)公司1987年推出的。

1991年,伴随着MS-DOS5.0的推出,微软(Microsoft)公司同时推出了Quick BASIC的简化版QBASIC,将其作为操作系统的组成部分免费提供给用户。自从Windows操作系统出现以来,图形用户界面(GUI)的BASIC语言(即Visual Basic)已经得到广泛应用。

2001年Visual Basic .NET推出

2003年推出Visual Basic .NET 2003推出

2005年11月7日在Visual Studio 2005内推出Visual Basic 2005。

BASIC语言早期是以直译程式的方式创始,也演化出许多不同名称的版本,如:BASICA,GW-BASIC,MBASIC,TBASIC,...。微软公司也在MS-DOS时代即推出QuickBASIC,并逐渐将之改良为兼具直译与编译双重翻译方式,1988年在Windows开始流行的时候,微软公司推出VisualBasicforWindows成为Windows作业环境一枝独秀的易学易用程式语言,微软公司还特地为MS-DOS的使用者开发了VisualBasicforMSDOS。微软在早期的崛起BASIC语言功不可没。

Visual Basic 6.0编译出的程序可以顺利运行在Windows 7下,也可以顺利运行在微软最新推出的操作系统Windows 8下。

3 名字 编辑
Beginner's All-purpose Symbolic Instruction Code(初学者通用的符号指令代码),刚开始被作者写做BASIC,后来被微软广泛地叫做Basic。

BASIC语言本来是为校园的大学生们创造的高级语言,目的是使大学生容易使用计算机。尽管初期的BASIC仅有16条语句,但由于BASIC在当时比较容易学习,它很快从校园走向社会,成为初学者学习计算机程序设计的首选语言。

1975 年,比尔·盖茨创立的 Microsoft,并成功的把Basic语言的编译器移植到使用 Intel处理器的 ALR 计算机中,IBM 在 1982 年选定 Microsoft 创作 PC 的操作系统时,也选定了 Microsoft 的Basic作为其计算机的 ROM-Basic。微软还在其发布的 DOS 操作系统中免费加入了 GW-Basic、QBasic 等当时最好的Basic解释程序。

2001年Visual Basic .NET推出

2003年推出Visual Basic .NET 2003推出

2005年11月7日在Visual Studio .NET 2005内推出Visual Basic .NET 2005。

还有Visual Studio .NET 2008里有Visual Basic .NET 2008

4 关于 编辑
BASIC是在1965年5月,由美国科学家托马斯·库尔兹研制出来的。10多年后,(前微软公司的总裁)比尔·盖茨把它移植到PC上。三十多年来,BASIC语言一直是初学计算机语言者使用最广泛的一种高级语言。它能进行数值计算、画图、演奏音乐,功能十分强大,而学起来又是非常容易。

5 基本命令 编辑
PRINT:显示内容或结果

INPUT:键入

LET:赋值

GOTO:无条件转移

FOR TO……NEXT:循环

IF THEN ELSE:条件

DO WHILE……LOOP:条件循环

END:结束

RUN:运行

CLS:清屏

6 语言特点 编辑
1.简单易学:BASIC语言所使用的词大多数是英语单词的原意或缩写,运算符号、表达式的书写也与数学中差不多。标准的BASIC语句只有17种。

2.会话式:人们可以通过键盘和显示屏与计算机“对话”,运行程序时,计算机会把程序中语法错误及错误的属性显示出来,让使用者修改。

3.适用面广:既能进行科学计算,又能数据处理等。

4.两种执行方式:解释方式和编译方式。其中解释方式可以边输程序边运行非常适合初学者。

BASIC是Beginner's All-purpose Symbolic Instruction Code的缩写,意为初学者通用符号指令代码语言,它是在1964年由美国的两位教授Thomas和John G.Kemeny在Fortran语言的基础上设计的语言系统,这个简单、易学的程序设计语言当时只有17条语句,12个函数和3个命令,现在一般称其为基本BASIC。
--------------------------------------------------------------------------
quick basic
编辑词条
该词条缺少摘要、词条分类,补充相关内容帮助词条更加完善!立刻编辑>>
中文名        Quick Basic
性 质        BASIC变种       
外文名        Beginner's All-purpose Symbolic Instruction Code
单 位        美国微软公司


1概述
2改进
3常见语法
条件语句
循环结构语句
基本语句
变量
简单的双重循环在冒泡排序程序的运用
几种常见的运算语句及函数
初学者的练习题
4快捷键
5范例
1 概述 编辑
QBASIC是BASIC(Beginner's All-purpose Symbolic Instruction Code,初学者通用指令代码)语言的一个变种,由美国微软公司开发, 1991年随MS-DOS 5.0推出。它不能被编译成独立的 可执行文件, 源代码在 集成开发环境(IDE)中先被编译成 中间代码,然后中间代码在IDE中被 解释执行。它被设计用来代替GW-BASIC,并被集成在MS-DOS 5.0及其更高版本(包括Windows 95)中。QBASIC基于 微软稍早推出的QuickBASIC 4.5,但去掉了后者的编译和连接部分。

微软在较新版本的Windows中不再集成QBASIC。不过 Windows 98的 用户可以在 光盘的\\TOOLS\\OLDMSDOS目录中找到它,在 Windows 95的光盘中,它存放在\\OTHER\\OLDMSDOS目录中。 微软网站对它的技术支持只对MS-DOS的授权用户有效。

QBASIC拥有一个值得称道的 集成开发环境和一个功能强大的集成调试器,这一切在那个时代让人耳目一新。直到今天,QBASIC依然是许多面向初学者的编程书籍的 主题。

2 改进 编辑
和Quick BASIC类似而又不同于 微软其他BASIC的早期实现版本的是,QBASIC是一种结构化的 编程语言。和GW-BASIC相比,QBASIC的主要改进是:

扩充了变量和常量的类型

变量名长度:40个 字符

增加了 长整型、定长字符型 变量

可定义数值常量、 字符串常量

子程序和函数作为单独的模块

不需要行号

注:Quick BASIC简称QB,是一种编译型的语言; qbasic是一种解释型的语言,和Quick BASIC拥有一样的语法。同时,为了省时间,QBASIC中的PRINT 语句可以直接用?来代替。

3 常见语法 编辑
条件语句
行if语句: IF 条件 THEN 语句组 ELSE 语句组2

块if语句

IF 条件 THEN

语句组

ELSE

语句组2

END IF

IF 语句还可如此运用

IF 条件 THEN

语句组

ELSEIF 条件 THEN

语句组2

END IF

多分支语句SELECT用法:

SELECT CASE 变量或字符串

CASE 情况1

语句组

CASE 情况2

语句组2

END SELECT

循环结构语句
1.计数循环

for 控制变量=初值 to 终值 'step语句可有可无,若没有step语句,则步长为+1

语句体

next 控制变量

2.当型循环

其格式有两种:

(1)

WHILE 条件

循环体

WEND

(2)

DO WHILE 条件

循环体

LOOP

3.直到型循环

DO

循环体

LOOP UNTIL 条件

基本语句
CLS:即Clean the screen,清屏幕

输入语句:input“显示的内容”, 变量名表

或者“,”改为“;”,改完之后输入时会多出一个“?”

输出语句:print “显示的内容”, 变量名表1,“显示的内容”,变量名表2……

把,改为;时,两个内容间空1格,否则空14格左右,最后不加分号为换行。

赋值语句: 被赋值变量= 表达式 'let 可有可无

一维 数组的定义:dim 变量名(下标)

二维数组的定义:dim 变量名(下标1,下标2)

代码示例(赋值,求和运算,并显示结果)

A=10

B=20

C=A+B

Print C

END

变量
变量长度小于等于40,不允许出现关键词,如Let

数:如15%、-32768、215654#、2.0158e+15、8.545646d+20、-18.75等, 变量名为例如a的变量名

字符串:如“15%”、“abc”等, 变量名为例如a$的变量名

补充:在QB中还可以几何画图,具体用法如下(详见QB内部帮助)

SCREEN 12'639*479 16色图形模式

CIRCLE(100,150),10,4'在x坐标100与y坐标150处画一个半径为10的颜色为4(红色)的圆。

SYSTEM

简单的双重循环在冒泡排序程序的运用
CLS

DIM n AS INTEGER

INPUT n

DIM a(n)

FOR i = 1 TO n

INPUT a(i)

NEXT i

FOR i = 1 TO n

FOR j=1 TO i-1

IF a(j) > a(j+1) THEN SWAP a(j), a(j+1)

NEXT j

NEXT i

FOR i = 1 TO n

PRINT a(i)

NEXT i

END

几种常见的运算语句及函数
加运算:

AB之和=A+B

差运算:

AB之差=A-B

乘运算:

AB之积=A*B

除运算

AB商=A/B

乘方运算

A的B次方=A^B

开方运算

A开方=SQR(A)

交换变量

SWAP 值A,值B

初学者的练习题
1、输入20个数,求出它们的最大值、最小值和平均值。

2、在1——500中,找出能同时满足用3除余2,用5除余3,用7除余2的所有整数;

3、如果一个数从左边读和右边读都是同一个数,就称为回文数,例如686就是一个回文数?喑糖?000以内所有的回文数。

4、已知数列1、5、12、22、35、...?求出第20个数

5、输入一个大于1的整数,打印出它的素数分解式。如输入75,则打印:"75=3*5*5"。

6、输入10个正整数,计算它们的和,平方和;

7、输入20个整数,统计其中正、负和零的个数;

8、输出1——999中能被3整除,且至少有一位数字是5的数;

9、有一个六位数,其个位数字7,现将个位数字移至首位(十万位),而其余各位数字顺序不变,得到一个新的六位数,假如新数为旧数的4倍,求原来的六位数。

10、有这样的一个六位数字labcde,将其乘以3后变成abcdel,编程求这个数。

11、试找出6个小于160而成等差数列的素数。

1-1/3+1/5-1/7+……直到某一项的绝对值小于10的-6次方

附加:

我们用一个正整数列来表示一段地方的高度,当一段地方的高度为一个逐一上升的序列时,

我们称它为一个阶梯,例如 4、5、6、7、8 是一个长度为 5 的阶梯。现在给定一个正整数列,

请找出它第一个最长的阶梯,并将其输出。如果一个阶梯也没有,输出“No”。

运行结果示例:

请输入数列的长度:8

请输入数列:2 3 2 3 4 4 5 6

结果为 2 3 4

4 快捷键 编辑
Ctrl+C+Break:中断正在运行的程序;

F5:运行程序;

Shift+F5:从第一条语句开始重新运行程序;

F4:当程序中断运行时,查看运行结果屏幕,再按一次F4则切换回代码屏幕;

F1:获得帮助。

F8: 单步运行

F9:断点(同QB stop 语句,按F5继续运行)

5 范例 编辑
【1】菜场上一公斤香菇是7.5元,编一个程序,从键盘上输入重量,计算机自动算出其总价

INPUT X

zj=7.5*X

PRINT zj

END

【2】高精度乘法程序

CLS

INPUT a$

INPUT b$

la = LEN(a$)

lb = LEN(b$)

lc = la + lb

DIM a(la), b(lb), c(lc)

FOR i = 1 TO la

a(i) = VAL(MID$(a$, la + 1 - i, 1))

NEXT i

FOR i = 1 TO lb

b(i) = VAL(MID$(b$, lb + 1 - i, 1))

NEXT i

FOR i = 1 TO la

FOR j = 1 TO lb

x = a(i) * b(j): w = i + j - 1

c(w) = c(w) + x MOD 10

c(w + 1) = c(w + 1) + c(w) \ 10 + x \ 10

c(w) = c(w) MOD 10

NEXT j

NEXT i

DO WHILE c(lc) = 0

lc = lc - 1

LOOP

FOR i = lc TO 1 STEP -1

PRINT USING "#"; c(i);

NEXT i

END
-----------------------------------------------------
gw-basic
编辑词条
该词条缺少摘要图、词条分类,补充相关内容帮助词条更加完善!立刻编辑>>
GW-BASIC是高级程序设计语言BASIC的一个方言版本。关于GW的含义,目前有三种说法,一种认为是以微软早期程序员GregWhitten的名字命名的,一种认为是用微软创始人的名字Gates,William命名的,还有一种说法是开发人员给它取的一个戏称gee-whiz(两个英语的象声词,类似于汉语中吆喝牲口的声音)。有一点是肯定的,这个版本的BASIC最早是微软为康柏开发的,时间应该是在1984年,这一年11月,微软,这一年11月,微软首次为IBM之外的计算机厂商提供OEM版本的DOS,GW-BASIC是作为MS-DOS的一个组成部分问世的。首次为IBM之外的计算机厂商提供OEM版本的DOS,GW-BASIC是作为MS-DOS的一个组成部分问世的。
中文名        gw-basic
时 间        1984年       
性 质        程序设计语言
归 属        微软


1历史地位
2特性
3语法
1 历史地位 编辑
Basic的发展经历了三个阶段 :

非结构化Basic语言,如:gw-Basic、MS Basic

结构化Basic语言,如:True Basic、Turbo Basic、Quick Basic

面向对象的程序设计语言,即Visual Basic

gw-basic属于非结构化basic,也就是最早期的basic语言,没有loop和while 循环语句,局限性很大。

2 特性 编辑
GW-BASIC和 微软提供给IBM PC的 BASICA完全兼容,所不同的是,后者依赖于ROM中的BASIC解释器,而前者并不需要,所以,GW-BASIC能在众多IBM PC 兼容机上运行,这就使它的使用范围随着PC兼容机的推广而扩大。第一个流传范围较广的GW-BASIC版本号为2.0,而最后推出的GW-BASIC版本号为3.23,时间大概是1988年。从那时起,如果没有特殊说明,我们一般所说的GW-BASIC就是指的这个版本。

GW-BASIC运行速度较慢,这在很大程度上是因为它是一种交互式的开发工具——这种开发模式是BASIC的诞生地Dartmouth大学最早提出的。和很多早期的BASIC方言一样,GW-BASIC缺乏很多进行 结构化编程所需的语法成分,但是它足够灵活,此外还拥有很多绘图语句和一些简单的发声语句,这就足以使一个 程序员用它开发一个简单的游戏软件、商用软件或者诸如此类的东西。它能够在绝大多数PC上运行,这就给那些想要成为 程序员的人提供了一个学习如何编程的廉价的途径。

3 语法 编辑
GW-BASIC拥有一个简单的 集成开发环境(IDE),所有的程序行都必须有一个 行号,没有行号的语句被认为是需要立即执行的命令。用户界面中,除 屏幕底部的功能快捷键描述和顶部的版权声明外,其他部分都用来显示和编写语句。源文件标准的保存格式是GW-BASIC特有的二进制压缩格式,不过它也提供了一个选项,使得开发者可以用ASCII 文本文件格式保存源文件。GW-BASIC的IDE提供了如下常用命令:RUN(执行当前 源代码),LOAD(从 磁盘装入源代码),SAVE(把源代码保存到磁盘),LIST(显示已打开的源文件内容),SYSTEM(返回操作系统)。它们都可以出现在 源程序代码行上,不过除SYSTEM外,上述用法还相当少见。

前面提到,GW-BASIC对 结构化程序设计方法的支持非常差,所以对GW-BASIC程序员来讲,能够用它写出具备良好结构的程序是很大的提高。在GW-BASIC里,IF/THEN/ELSE条件语句必须在一行内写完,尽管WHILE/WEND已允许包含多行代码;自定义函数只能用类似于DEF FNf(x) = <关于x的数学函数> 这样形式的一行语句来编写(例如,DEF FNLOG(base,number)=LOG(number)/LOG(base)); 变量通常是通过变量名末端的一个表示类型的符号来确定其类型的:A$表示是一个字符串,A%表示是一个 整型数,等等;通过使用DEFINT,DEFSTR等关键字,可以为一组使用相同首字母的变量定义缺省类型;其他变量其类型默认是单精度浮点数。

很多GW-BASIC程序员都是没有经过训练的,他们往往看不到编写结构简单的程序所能带来的好处,因此就导致滥用GOTO语句的现象,他们往往不愿意使用能完成同样功能的结构化的语句。参见面条式代码。

GW-BASIC支持游戏操纵杆和 光笔输入设备,但不支持鼠标。它能够读写磁盘文件、LPT端口和COM端口,也能处理端口事件陷阱,不过不能处理磁带设备。它还能通过使用PLAY语句或SOUND语句来驱动IBM PC及其 兼容机的标准内置扬声器发出声音。
-------------------------------------------------------------------



1<词>,2[句],3/段\,4{节},5(章)。
2016-2-6 07:25
查看资料  发短消息 网志   编辑帖子  回复  引用回复
zzz19760225
超级版主




积分 3673
发帖 2020
注册 2016-2-1
状态 离线
『第 15 楼』:  

电平触发,就是只有高电平(或者低电平)的时候才做指定的事,
边沿触发,就是有高电平向低电平转换,或者翻过来转换,这个转换过程触发一个动作。
上升沿,顾名思义,就是低电平向高电平转换的瞬间(过程),比如
_______
____/ ,这个图中,/部分就是上升沿,

______
\_____ ,这个图中,\部分就是下降沿
---------------------------------------------------------------

电平就是逻辑上的0,1触发,

而边沿就是脉冲突变触发,逻辑上就是0-1或是1-0 也就是上楼的那位表示的

通俗点讲吧
电平就是电压,高电平就是高电压,低电平就是低电压高电平触发就是当电压为高是触发边沿触发就是当电压由高变低或由低变高时触发
上升沿触发 就是当电压从低变高时触发
下降沿触发 就是当电压从高变低时触发
luts3702 2006-05-13 08:32
计算机处理的是数字信号,即二进制数0和1.这一般是利用电子器件的接通与断开二种状态实现的,当电子器件截止时,输出电位抬高,用它代表数字1,故又称为高电平.当器件接通时,用它代表数字0,故又称为低电平.
分享本回答由科学教育分类达人 任纪兰认证

您当前的位置:首页 > 基础知识 > 模拟 > 模拟技术常见问题
什么是TTL电平、CMOS电平?区别?
来源:21ic 作者:
关键字:CMOS   TTL   电平      
什么是ttl电平

TTL电平信号被利用的最多是因为通常数据表示采用二进制规定,+5V等价于逻辑"1",0V等价于逻辑"0",这被称做TTL(晶体管-晶体管逻辑电平)信号系统,这是计算机处理器控制的设备内部各部分之间通信的标准技术。

TTL电平信号对于计算机处理器控制的设备内部的数据传输是很理想的,首先计算机处理器控制的设备内部的数据传输对于电源的要求不高以及热损耗也较低,另外TTL电平信号直接与集成电路连接而不需要价格昂贵的线路驱动器以及接收器电路;再者,计算机处理器控制的设备内部的数据传输是在高速下进行的,而TTL接口的操作恰能满足这个要求。TTL型通信大多数情况下,是采用并行数据传输方式,而并行数据传输对于超过10英尺的距离就不适合了。这是由于可靠性和成本两面的原因。因为在并行接口中存在着偏相和不对称的问题,这些问题对可靠性均有影响;另外对于并行数据传输,电缆以及连接器的费用比起串行通信方式来也要高一些。

TTL电路的电平就叫TTL 电平,CMOS电路的电平就叫CMOS电平

TTL集成电路的全名是晶体管-晶体管逻辑集成电路(Transistor-Transistor Logic),主要有54/74系列标准TTL、高速型TTL(H-TTL)、低功耗型TTL(L-TTL)、肖特基型TTL(S-TTL)、低功耗肖特基型TTL(LS-TTL)五个系列。标准TTL输入高电平最小2V,输出高电平最小2.4V,典型值3.4V,输入低电平最大0.8V,输出低电平最大0.4V,典型值0.2V。S-TTL输入高电平最小2V,输出高电平最小Ⅰ类2.5V,Ⅱ、Ⅲ类2.7V,典型值3.4V,输入低电平最大0.8V,输出低电平最大0.5V。LS-TTL输入高电平最小2V,输出高电平最小Ⅰ类2.5V,Ⅱ、Ⅲ类2.7V,典型值3.4V,输入低电平最大Ⅰ类0.7V,Ⅱ、Ⅲ类0.8V,输出低电平最大Ⅰ类0.4V,Ⅱ、Ⅲ类0.5V,典型值0.25V。TTL电路的电源VDD供电只允许在+5V±10%范围内,扇出数为10个以下TTL门电路;

COMS集成电路是互补对称金属氧化物半导体(Compiementary symmetry metal oxide semicoductor)集成电路的英文缩写,电路的许多基本逻辑单元都是用增强型PMOS晶体管和增强型NMOS管按照互补对称形式连接的,静态功耗很小。COMS电路的供电电压VDD范围比较广在+5--+15V均能正常工作,电压波动允许±10,当输出电压高于VDD-0.5V时为逻辑1,输出电压低于VSS+0.5V(VSS为数字地)为逻辑0,扇出数为10--20个COMS门电路.

TTL电平信号被利用的最多是因为通常数据表示采用二进制规定,+5V等价于逻辑"1",0V等价于逻辑"0",这被称做TTL(晶体管-晶体管逻辑电平)信号系统,这是计算机处理器控制的设备内部各部分之间通信的标准技术。TTL电平信号对于计算机处理器控制的设备内部的数据传输是很理想的,首先计算机处理器控制的设备内部的数据传输对于电源的要求不高以及热损耗也较低,另外TTL电平信号直接与集成电路连接而不需要价格昂贵的线路驱动器以及接收器电路;再者,计算机处理器控制的设备内部的数据传输是在高速下进行的,而TTL接口的操作恰能满足这个要求。TTL型通信大多数情况下,是采用并行数据传输方式,而并行数据传输对于超过10英尺的距离就不适合了。这是由于可靠性和成本两面的原因。因为在并行接口中存在着偏相和不对称的问题,这些问题对可靠性均有影响;另外对于并行数据传输,电缆以及连接器的费用比起串行通信方式来也要高一些。CMOS电平和TTL电平: CMOS电平电压范围在3~15V,比如4000系列当5V供电时,输出在4.6以上为高电平,输出在0.05V以下为低电平。输入在3.5V以上为高电平,输入在1.5V以下为低电平。而对于TTL芯片,供电范围在0~5V,常见都是5V,如74系列5V供电,输出在2.7V以上为高电平,输出在0.5V以下为低电平,输入在2V以上为高电平,在0.8V以下为低电平。因此,CMOS电路与TTL电路就有一个电平转换的问题,使两者电平域值能匹配

TTL电平与CMOS电平的区别:

(一)TTL高电平3.6~5V,低电平0V~2.4V

CMOS电平Vcc可达到12V

CMOS电路输出高电平约为0.9Vcc,而输出低电平约为0.1Vcc。

CMOS电路不使用的输入端不能悬空,会造成逻辑混乱。

TTL电路不使用的输入端悬空为高电平

另外,CMOS集成电路电源电压可以在较大范围内变化,因而对电源的要求不像TTL集成电路那样严格。

用TTL电平他们就可以兼容

(二)TTL电平是5V,CMOS电平一般是12V。

因为TTL电路电源电压是5V,CMOS电路电源电压一般是12V。

5V的电平不能触发CMOS电路,12V的电平会损坏TTL电路,因此不能互相兼容匹配。

(三)TTL电平标准

输出 L: <0.8V ; H:>2.4V。

输入 L: <1.2V ; H:>2.0V

TTL器件输出低电平要小于0.8V,高电平要大于2.4V。输入,低于1.2V就认为是0,高于2.0就认为是1。

CMOS电平:

输出 L: <0.1*Vcc ; H:>0.9*Vcc。

输入 L: <0.3*Vcc ; H:>0.7*Vcc.

一般单片机、DSP、FPGA他们之间管教能否直接相连. 一般情况下,同电压的是可以的,不过最好是要好好查查技术手册上的VIL,VIH,VOL,VOH的值,看是否能够匹配(VOL要小于VIL,VOH要大于VIH,是指一个连接当中的)。有些在一般应用中没有问题,但是参数上就是有点不够匹配,在某些情况下可能就不够稳定,或者不同批次的器件就不能运行。

例如:74LS的器件的输出,接入74HC的器件。在一般情况下都能好好运行,但是,在参数上却是不匹配的,有些情况下就不能运行。

74LS和54系列是TTL电路,74HC是CMOS电路。如果它们的序号相同,则逻辑功能一样,但电气性能和动态性能略有不同。如,TTL的逻辑高电平为> 2.7V,CMOS为> 3.6V。如果CMOS电路的前一级为TTL则隐藏着不可靠隐患,反之则没问题。

1,TTL电平:

输出高电平>2.4V,输出低电平<0.4V。在室温下,一般输出高电平是3.5V,输出低电平是0.2V。最小输入高电平和低电平:输入高电平>=2.0V,输入低电平<=0.8V,噪声容限是0.4V。

2,CMOS电平:

1逻辑电平电压接近于电源电压,0逻辑电平接近于0V。而且具有很宽的噪声容限。

3,电平转换电路:

因为TTL和COMS的高低电平的值不一样(ttl 5v<==>cmos 3.3v),所以互相连接时需要电平的转换:就是用两个电阻对电平分压,没有什么高深的东西。哈哈

4,OC门,即集电极开路门电路,OD门,即漏极开路门电路,必须外界上拉电阻和电源才能将开关电平作为高低电平用。否则它一般只作为开关大电压和大电流负载,所以又叫做驱动门电路。

5,TTL和COMS电路比较:

1)TTL电路是电流控制器件,而coms电路是电压控制器件。

2)TTL电路的速度快,传输延迟时间短(5-10ns),但是功耗大。COMS电路的速度慢,传输延迟时间长(25-50ns),但功耗低。COMS电路本身的功耗与输入信号的脉冲频率有关,频率越高,芯片集越热,这是正常现象。

3)COMS电路的锁定效应:

COMS电路由于输入太大的电流,内部的电流急剧增大,除非切断电源,电流一直在增大。这种效应就是锁定效应。当产生锁定效应时,COMS的内部电流能达到40mA以上,很容易烧毁芯片。

防御措施:

1)在输入端和输出端加钳位电路,使输入和输出不超过不超过规定电压。

2)芯片的电源输入端加去耦电路,防止VDD端出现瞬间的高压。

3)在VDD和外电源之间加线流电阻,即使有大的电流也不让它进去。

4)当系统由几个电源分别供电时,开关要按下列顺序:开启时,先开启COMS电路得电源,再开启输入信号和负载的电源;关闭时,先关闭输入信号和负载的电源,再关闭COMS电路的电源。

6,COMS电路的使用注意事项

1)COMS电路时电压控制器件,它的输入总抗很大,对干扰信号的捕捉能力很强。所以,不用的管脚不要悬空,要接上拉电阻或者下拉电阻,给它一个恒定的电平。

2)输入端接低内组的信号源时,要在输入端和信号源之间要串联限流电阻,使输入的电流限制在1mA之内。

3)当接长信号传输线时,在COMS电路端接匹配电阻。

4)当输入端接大电容时,应该在输入端和电容间接保护电阻。电阻值为R=V0/1mA.V0是外界电容上的电压。

5)COMS的输入电流超过1mA,就有可能烧坏COMS。

7,TTL门电路中输入端负载特性(输入端带电阻特殊情况的处理):

1)悬空时相当于输入端接高电平。因为这时可以看作是输入端接一个无穷大的电阻。

2)在门电路输入端串联10K电阻后再输入低电平,输入端出呈现的是高电平而不是低电平。因为由TTL门电路的输入端负载特性可知,只有在输入端接的串联电阻小于910欧时,它输入来的低电平信号才能被门电路识别出来,串联电阻再大的话输入端就一直呈现高电平。这个一定要注意。COMS门电路就不用考虑这些了。

8,TTL电路有集电极开路OC门,MOS管也有和集电极对应的漏极开路的OD门,它的输出就叫做开漏输出。OC门在截止时有漏电流输出,那就是漏电流,为什么有漏电流呢?那是因为当三机管截止的时候,它的基极电流约等于0,但是并不是真正的为0,经过三极管的集电极的电流也就不是真正的0,而是约0。而这个就是漏电流。开漏输出:OC门的输出就是开漏输出;OD门的输出也是开漏输出。它可以吸收很大的电流,但是不能向外输出的电流。

所以,为了能输入和输出电流,它使用的时候要跟电源和上拉电阻一齐用。OD门一般作为输出缓冲/驱动器、电平转换器以及满足吸收大负载电流的需要。

9,什么叫做图腾柱,它与开漏电路有什么区别?

TTL集成电路中,输出有接上拉三极管的输出叫做图腾柱输出,没有的叫做OC门。因为TTL就是一个三级关,图腾柱也就是两个三级管推挽相连。所以推挽就是图腾。

扩展阅读:电感和磁珠的区别与联系


TTL电平 编辑
TTL电平信号被利用的最多是因为通常数据表示采用二进制规定,+5V等价于逻辑“1”,0V等价于逻辑“0”,这被称做TTL(晶体管-晶体管逻辑电平)信号系统,这是计算机处理器控制的设备内部各部分之间通信的标准技术。
中文名 TTL电平 外文名 transistor transistor logic 规    定 二进制 又    称 晶体管-晶体管逻辑电平 应    用计算机处理器
目录
1 优点
2 标准
3 涉及学科
▪ 与CMOS管
▪ 附
优点编辑
TTL电平信号对于计算机处理器控制的设备内部的数据传输是很理想的,首先计算机处理器控制的设备内部的数据传输对于电源的要求不高以及热损耗也较低,另外TTL电平信号直接与集成电路连接而不需要价格昂贵的线路驱动器以及接收器电路;再者,计算机处理器控制的设备内部的数据传输是在高速下进行的,而TTL接口的操作恰能满足这个要求。TTL型通信大多数情况下,是采用并行数据传输方式,而并行数据传输对于超过10英尺的距离就不适合了。这是由于可靠性和成本两面的原因。因为在并行接口中存在着偏相和不对称的问题,这些问题对可靠性均有影响。
数字电路中,由TTL电子元器件组成电路使用的电平。电平是个电压范围,规定输出高电平>2.4V,输出低电平<0.4V。在室温下,一般输出高电平是3.5V,输出低电平是0.2V。最小输入高电平和低电平:输入高电平>=2.0V,输入低电平<=0.8V,噪声容限是0.4V。
标准编辑
“TTL集成电路的全名是晶体管-晶体管逻辑集成电路(Transistor-Transistor Logic),主要有54/74系列标准TTL、高速型TTL(H-TTL)、低功耗型TTL(L-TTL)、肖特基型TTL(S-TTL)、低功耗肖特基型TTL(LS-TTL)五个系列。标准TTL输入高电平最小2V,输出高电平最小2.4V,典型值3.4V,输入低电平最大0.8V,输出低电平最大0.4V,典型值0.2V。S-TTL输入高电平最小2V,输出高电平最小Ⅰ类2.5V,Ⅱ、Ⅲ类2.7V,典型值3.4V,输入低电平最大0.8V,输出低电平最大0.5V。LS-TTL输入高电平最小2V,输出高电平最小Ⅰ类2.5V,Ⅱ、Ⅲ类2.7V,典型值3.4V,输入低电平最大Ⅰ类0.7V,Ⅱ、Ⅲ类0.8V,输出低电平最大Ⅰ类0.4V,Ⅱ、Ⅲ类0.5V,典型值0.25V。”
涉及学科编辑
“TTL电平”最常用于有关电专业,如:电路、数字电路、微机原理与接口技术、单片机等课程中都有所涉及。在数字电路中只有两种电平(高和低)高电平+5V、低电平0V。同样运用比较广泛的还有CMOS电平、232电平、485电平等。
与CMOS管

1.CMOS是场效应管构成,TTL为双极晶体管构成
  2.CMOS的逻辑电平范围比较大(3~15V),TTL只能在5V下工作
  3.CMOS的高低电平之间相差比较大、抗干扰性强,TTL则相差小,抗干扰能力差
  4.CMOS功耗很小,TTL功耗较大(1~5mA/门)
  5.CMOS的工作频率较TTL略低,但是高速CMOS速度与TTL差不多相当
简单理解:
TTL电平,TTL的电源工作电压是5V,所以TTL的电平是根据电源电压5V来定的。CMOS电平,CMOS的电源工作电压是3V - 18V,CMOS的电源工作电压范围宽,如果你的CMOS的电源工作电压是12V,那么这个CMOS的输入输出电平电压要适合12V的输入输出要求。即CMOS的电平,要看你用的电源工作电压是多少,3v - 18V,都在CMOS的电源工作电压范围内,具体数值,看你加在CMOS芯片上的电源工作电压是多少。


详解TTl和cmos差异
TTL——Transistor-Transistor Logic
  HTTL——High-speed TTL
  LTTL——Low-power TTL
  STTL——Schottky TTL
  LSTTL——Low-power Schottky TTL
  ASTTL——Advanced Schottky TTL
  ALSTTL——Advanced Low-power Schottky TTL
  FAST(F)——Fairchild Advanced schottky TTL
  CMOS——Complementary metal-oxide-semiconductor
  HC/HCT——High-speed CMOS Logic(HCT与TTL电平兼容)
  AC/ACT——Advanced CMOS Logic(ACT与TTL电平兼容)(亦称ACL)
  AHC/AHCT——Advanced High-speed CMOS Logic(AHCT与TTL电平兼容)
  FCT——FACT扩展系列,与TTL电平兼容
  FACT——Fairchild Advanced CMOS Technology
  1,TTL电平:
  输出高电平>2.4V,输出低电平<0.4V。在室温下,一般输出高电平是3.5V,输出低电平
  是0.2V。最小输入高电平和低电平:输入高电平>=2.0V,输入低电平<=0.8V,噪声容限是
  0.4V。
  2,CMOS电平:
  1逻辑电平电压接近于电源电压,0逻辑电平接近于0V。而且具有很宽的噪声容限。
  3,电平转换电路:
  因为TTL和COMS的高低电平的值不一样(ttl 5v<==>cmos 3.3v),所以互相连接时需
  要电平的转换:就是用两个电阻对电平分压,没有什么高深的东西。
  4,OC门,即集电极开路门电路,OD门,即漏极开路门电路,必须外接上拉电阻和电源才能
  将开关电平作为高低电平用。否则它一般只作为开关大电压和大电流负载,所以又叫做驱
  动门电路。
  5,TTL和CMOS电路比较:
1)TTL电路是电流控制器件,而CMOS电路是电压控制器件。
  2)TTL电路的速度快,传输延迟时间短(5-10ns),但是功耗大。
  CMOS电路的速度慢,传输延迟时间长(25-50ns),但功耗低。
  CMOS电路本身的功耗与输入信号的脉冲频率有关,频率越高,芯片集越热,这是正常
  现象。
  3)CMOS电路的锁定效应:
  CMOS电路由于输入太大的电流,内部的电流急剧增大,除非切断电源,电流一直在增大
  。这种效应就是锁定效应。当产生锁定效应时,CMOS的内部电流能达到40mA以上,很容易
  烧毁芯片。
  防御措施:
  1)在输入端和输出端加钳位电路,使输入和输出不超过规定电压。
  2)芯片的电源输入端加去耦电路,防止VDD端出现瞬间的高压。
  3)在VDD和外电源之间加限流电阻,即使有大的电流也不让它进去。
  4)当系统由几个电源分别供电时,开关要按下列顺序:开启时,先开启CMOS电路的电
源,再开启输入信号和负载的电源;关闭时,先关闭输入信号和负载的电源,再关闭CMOS
电路的电源。
  6,CMOS电路的使用注意事项
  1)CMOS电路时电压控制器件,它的输入总抗很大,对干扰信号的捕捉能力很强。所以
  ,不用的管脚不要悬空,要接上拉电阻或者下拉电阻,给它一个恒定的电平。
  2)输入端接低内阻的信号源时,要在输入端和信号源之间要串联限流电阻,使输入的
  电流限制在1mA之内。
  3)当接长信号传输线时,在CMOS电路端接匹配电阻。
  4)当输入端接大电容时,应该在输入端和电容间接保护电阻。电阻值为R=V0/1mA.V0是
  外界电容上的电压。
  5)CMOS的输入电流超过1mA,就有可能烧坏CMOS。
  7,TTL门电路中输入端负载特性(输入端带电阻特殊情况的处理):
  1)悬空时相当于输入端接高电平。因为这时可以看作是输入端接一个无穷大的电阻。
  2)在门电路输入端串联10K电阻后再输入低电平,输入端出呈现的是高电平而不是低电
  平。因为由TTL门电路的输入端负载特性可知,只有在输入端接的串联电阻小于910欧时,
  它输入来的低电平信号才能被门电路识别出来,串联电阻再大的话输入端就一直呈现高电
  平。这个一定要注意。CMOS门电路就不用考虑这些了。
  8,TTL电路有集电极开路OC门,MOS管也有和集电极对应的漏极开路的OD门,它的输出就叫
  做开漏输出。
  OC门在截止时有漏电流输出,那就是漏电流,为什么有漏电流呢?那是因为当三极管截
  止的时候,它的基极电流约等于0,但是并不是真正的为0,经过三极管的集电极的电流也
  就不是真正的0,而是约0。而这个就是漏电流。开漏输出:OC门的输出就是开漏输出;OD
  门的输出也是开漏输出。它可以吸收很大的电流,但是不能向外输出的电流。所以,为了
  能输入和输出电流,它使用的时候要跟电源和上拉电阻一齐用。OD门一般作为输出缓冲/驱
  动器、电平转换器以及满足吸收大负载电流的需要。
  9,什么叫做图腾柱,它与开漏电路有什么区别?
  TTL集成电路中,输出有接上拉三极管的输出叫做图腾柱输出,没有的叫做OC门。因为
  TTL就是一个三级管,图腾柱也就是两个三级管推挽相连。所以推挽就是图腾。一般图腾式
  输出,高电平400UA,低电平8MA
词条标签: 科技产品 , 科学 , 书籍

[ Last edited by zzz19760225 on 2016-2-6 at 22:05 ]



1<词>,2[句],3/段\,4{节},5(章)。
2016-2-6 21:33
查看资料  发短消息 网志   编辑帖子  回复  引用回复
« [1] [2] [3] [4] [5] [6] »
请注意:您目前尚未注册或登录,请您注册登录以使用论坛的各项功能,例如发表和回复帖子等。


可打印版本 | 推荐给朋友 | 订阅主题 | 收藏主题



论坛跳转: