Electronic Joint Business

Solution for E-Business

编译与虚拟机

用汇编来学习 PC 的实模式、保护模式和长模式

本文主要面向那些有兴趣了解 CPU 是如何工作的用户,这里我将解释一些汇编语言基础知识、实模式、保护的模式和长模式。在文章的后面我还提供了完整的汇编代码,你可以用来测试看看处理器是如何在实模式下工作,如何进入保护模式,如何进入 64 位模式,最后又是如何从所有模式退出返回到 DOS。1

准备
要阅读本文以及进行相应的测试,你可能需要准备:

  • 汇编知识。虽然读者不需要编写代码,不过如果你对寄存器、内存访问、基本命令等有些了解会很有帮助。
  • Flat 汇编器。一个能生成 Win32, X64 和 DOS 下的可执行文件的“现代”汇编器。
  • 干净的 DOS 环境,你可以使用freeDOS

你无需在物理机上安装使用 DOS,相反你可以使用 VMWare 或者 Bochs 这样的虚拟机。这里我推荐使用 Bochs,不仅因为它是免费的,而且它的调试器可以陷入任何程序产生的异常,并告诉你到底发生了什么。

过去 Borland 的 Turbo 汇编器 (TASM) 十分流行,不过随着 Borland 的谢幕,TASM 也寿终正寝。附件中的所有代码都是用 FASM 编写的,该汇编器可以生成 16位的 DOS 以及 32/64位的 Windows 下的可执行文件。

你还可以在 Visual Studio 中找到 ML.exe 和 ML64.exe,这是 MASM 的新版本。不过该汇编器只能产生对应架构的 PE 格式,不适合这里的使用场景。

>>> 阅读全文

 

, , , ,

操作系统开发初体验(2)C++ 支持代码和控制台

在开始阅读第二篇文章前,我建议你翻阅上一篇文章,不然,接下来的内容可能就用处不大了。

现在我们需要一些支持代码,G++ 会用到其中的一些,不过我们增加了更多的代码以避免奇怪的错误。这里我们要添加的代码有:

  • 对构造器的调用
  • 提供 new 和 delete 的实现
  • 在 GCC 无法调用虚拟方法的时候提供一个方法以供调用。

除了上述三个,还有就是:据说有些操作系统会在内核代码结束的地方调用析构函数,但我没看过这样的案例,大多数多任务操作系统都会在 main 函数的末尾进入无限循环,这样定时器 IRQ 就可以来接管任务。(要晚很多)

首先,我们需要调用构造函数。链接脚本可以指出这些构造函数的指针起始和结束的地址。要调用它们,我们要做只是遍历它们。虽然链接器有可能会弄错顺序,不过概率很低。至少在本系列文章中,我们还是可以信任 GCC 的。

要调用构造器,我们可以使用下面的代码:

>>> 阅读全文

 

, ,

操作系统开发初体验 (1)

文章评价:
如果你正在读这篇文章,没准你想知道更多有关如何创建自己的 OS 方面的内容。不过你首先要知道,我这短短几篇文章是没办法涵盖 OS 开发的各个方面的。我会介绍一些 OS 的基础知识,但还需要靠你自己去研读相关的技术文档,这样你才能明白操作系统为何如此工作。确定还想读下去吗?好,让我们开始吧。

理解 OS 开发很关键的一点是:一切将从零开始,你直接和硬件打交道。你会遇到硬件故障,也不能指望系统每次都如期工作。你没有标准库可供使用,更没有 .NET 框架或者Java 虚拟机。一切只能靠你自己。

开始之前,你要了解系统控制权是如何转交到你手上的。很奇怪的是,没有什么标准规定电脑开机之后应置于什么状态,唯一能确定的是,开机之后一定会去执行引导扇区,它位于可启动的存储介质的起始部分,会被加载到内存地址 0x7C00 处,其长度为 512字节,并以 0xAA55 作为文件末尾签名。引导扇区通常用汇编语言编写。为了节约时间,我们将使用现成的 GRUB (Grand Unified Bootloader)。这可为后续工作提供可靠的基础,并将电脑置于同一个标准状态。

Grub 会将电脑置于以下状态:

  • 保护模式
  • A20 地址线启用
  • 寄存器 EBX 存有指向多重启动信息结构(Multiboot information structure)的指针
  • 寄存器 EAX 存有固定值 0x2BADB002
  • 分页机制关闭
  • 栈位于内存的某处
  • 中断机制关闭

下面我会逐一介绍这些状态。保护模式允许内核开发人员按虚拟地址空间访问最大为 4GB 的内存,并引入了保护环“Ring”的概念。以下几小节会介绍更多的内容。 A20 门电路是键盘控制器附近的一条“古老”的地址线。它最初设计是为了与 8086 保持兼容,在“关闭”的时候,屏蔽了对内存开始部分 1M 以上地址的访问。

>>> 阅读全文

 

, , , , , , , ,

Flex 和 Bison 简介 (三) 支持 C++

和一些收费软件相比,Flex 和 Bison 在某一些方面还是相对比较的弱的,比如说 Flex 对 C++ 的支持还处在实验状态,这意味着在将来的版本中有可能发生较大的改动,而且目前的解决方案在一些细节上处理得还并不好,但作为一个免费软件,Flex 和 Bison 总体上还是非常不错的软件,而且生成的词法生成器不依赖于其他库文件,可移植性比较好。

如果你更倾向于 Flex 和 Bison ,而不是用我们在第二节末尾提到的商业收费软件 Parser Generator 来生成资源模板解析器,那么请跟随本文来了解一下 Flex 和 Bison 是如何支持 C++ 的。

Flex 对 C++ 支持的基本是通过继承来完成的。在头文件 FlexLexer.h中定义了两个类 FlexLexer 和 yyFlexLexer。前者是一个抽象类,定义了 Flex 所生成的词法分析器的接口(interface),比如yylex();后者则继承自FlexLexer,是词法分析器的实现类,封装了词法分析器用到的状态变量等。因此,在默认的 C++ 模式下,Flex 的任务就是根据 “.l” 源文件自动生成 yyFlexLexer 中各成员函数的定义。规则的动作代码自然是被合成为 yyFlexLexer::yylex() 的实现啦,因此在规则动作中我们访问的变量和函数实际都是 yyFlexLexer 类的成员(或者是yylex()的参数 ),而不再是全局变量或全局函数。这便是 Flex 对 C++ 解决方案的基本原理。

这里还有一个问题。即在默认情况下,Flex 把生成的代码都放在了 yyFlexLexer 这一个类里,而事实上,实现类是应该有多个的,尤其是当我们的程序需要多个词法分析器的时候,每个词法分析器都应该对应一个实现类。Flex 是怎么解决这个问题的呢?事实上,在包含进这个头文件之前,”yyFlexLexer” 已经被定义为一个宏,其默认值为”yyFlexLexer”,而 Flex 提供了一些机制来重新定义这个宏,这样就能避免命名冲突的问题啦。关于这个问题我们先讲这么多,下面我们先详细讨论在默认情况下的具体做法,再回来讨论定制的问题。

为了让它跑起来,有两件最基本的事情是必须做的。首先,要在文件的定义段打开 c++ 选项,即添加 “%option c++” 一行。 或者在命令行使用 “-+” 选项也是一样的。打开这个选项之后,Flex 就会生成 C++ 代码,生成的代码会默认输出到 lex.yy.cc文件中(后缀不是”.c”啦)。可是如果这个时候直接编译(使用命令$ flex abc.l && g++ lex.yy.cc),则会收到链接出错的提示:”undefined reference to yyFlexLexer::yywrap()”。怎么回事呢?是不是忘记添库文件啦?仔细想一想,这个错误其实是在抱怨yyFlexLexer::yywrap()函数没有定义。原来,Flex 忘记帮我们生成一个默认的 yywrap() 的定义啦。其实这也不能怪 Flex 粗心,因为即便在 c 的情况下,它也不会自动合成这个函数的定义,我们之所以使用 C 语言时没有收到这个链接错误,只是因为在 fl 库中提供了一个默认的实现而已,因此如果我们自己不写,链接器就会把 fl 库中的那一个链接进来啦。可是,这种方法在 C++ 的环境下就不工作,因为这个是一个类的成员函数,而不是全局的啦。

>>> 阅读全文

 

, ,

编写 GCC 前端

文章评价:

这篇文章演示了如何为 GCC 创建新的前端的基本步骤,它可以帮你在 GCC 编译工具链的基础上创建自己的编译器。另外本文还囊括了一些基础工具的使用方法,如 Bison 和 Flex。阅读本文需要 C 语言的基础知识。你还可以参考介绍编译器基本原理的文章,这可以让你更好地理解相关的内容。

本文会演示如何在 GCC 编译工具链中添加一种新的迷你语言。虽然 GNU Internal 这部手册已经详细介绍了 GCC 的内部机理,但对新手来说,其内容实在多得可怕。通过本文的例子,新手也可以开始着手摆弄复杂的 GCC 基础代码。

编译器基础知识
编译器本质上就是个翻译器,它读入程序的源代码并将其转换成目标语言,目标语言通常是真实(或虚拟)CPU 汇编代码。设计一个真正的编译器是项复杂的工作,要求读过计算机科学和数学的正规课程。

整个编译过程可以分为几个子任务,我们称之为阶段,具体包括:

>>> 阅读全文

 

, , ,

Flex 和 Bison 简介 (二)

在上一篇文章中,我们讨论了如何用 flex 和 Bison 来 创建词法分析器(标记识别器)和解析器。本文是本系列文章的第二篇,我们将会看到如何用这些工具来创建可以读取 Visual Studio 6 资源文件的解析器。本文将重点关注于词法分析器。

不过考虑到这两个工具的密切联系,我们会在解析器和词法分析器之间来回跳转。在本文的最后,你可以下载到词法分析器和解析器的源代码。

如我们在第一篇文章中讨论的,词法分析器负责从某处读取输入流,并将其分解成一连串的标记。每个标记代表着最底层的构造块,用于表示诸如字符串、 数字或关键字等等东西。词法分析器通过将输入数据和一系列正则表达式 (规则) 相匹配来实现实现这一点。当它找到和特定规则的一个匹配时,它会执行将程序员所编写的一些执行代码,并向解析器返回一个标记,用于表明哪个规则被匹配了。它还可能会返回一些与规则关联的数据。

例如下面的正则表达式。

/* Decimal numbers */
-?[09]+    {
        yylval.ival = atoi(yytext);
        return NUMBER;
    }

该表达式会匹配以负号开头的包含至少一个数的十进制数序列。(有关正则表达式的含义和格式,你可以参考:http://www.monmouth.com/~wstreett/lex-yacc/flex_1.html 并搜索的”模式”)。当找到匹配时,它会调用 atoi 函数并将字符串 yytext 传递给它作为参数,yytext 正好是符合规则的字符串。另一方面,atoi 的返回值被赋给在解析器中定义的联合 yylval 的成员 ival。最后该操作会返回一个 NUMBER 标记给解析器并终止。

>>> 阅读全文

 

, , , , ,

Flex 和 Bison 简介(一)

文章评价:
我最近有个项目需要一个对话框编辑器,客户要求该编辑器能够加载对话框模板,并显示出相应的对话框。

具体的实现方法有简单的,也有复杂的。简单的方法只需将资源脚本编译成 .res 文件,然后让程序读取这些编译过的模板,由此得到对话框模板的句柄,并传递给 CreateDialogIndirect 函数。这样就可以用代码来检查每个控件的属性。但这种方法有个缺点:一旦修改过模板就必须重新编译资源脚本,否则将导致严重的逻辑错误。

有鉴于此,我舍易求难:实现一个能读取 Visual Studio6 资源文件 (.rc 文件)的解析器来检查每个控件的属性,然后再返回编译过的对话框模板。

如果你浏览过资源模板文件的源文件,你会发现自己动手从头开始实现这样一个解析器是不可能的。资源文件包含各种文本块,其中有注释、工具栏资源、菜单、对话框等等。每种文本块都有固定的格式。要用面向过程的方法来编写解析这些文本块的程序,要解决的问题实在多如牛毛。还好我们有 Flex 和 Bison 来提供支持。

什么是 Flex 和 Bison?
既然提到 Flex 和 Bison 就不能不说到其前身 Yacc 和 Lex 。Yacc 和 Lex 来自 UNIX。Yacc 是 “yet another compiler compiler” 的缩写,而 Lex 则是 ‘lexical analyser’ 的简称。仅仅它们的名称就已经说明了很多问题 !你可以参阅 compilertools 站点 ,该站点提供了适合不同水平用户的范例,而且还有很详细的说明和解释。

>>> 阅读全文

 

, , , , ,

Python编写的强大的通用解析器

Spark 是一种用 Python 编写的强大的、通用的解析器/编译器框架。在某些方面,Spark 所提供的比 SimpleParse 或其它 Python 解析器要多得多。不过由于它完全是用 Python 编写的,所以速度也会比较慢。

我将在本文中继续介绍一些解析的基本概念,并对 Spark 模块进行了讨论。解析框架是一个内容丰富的主题,它值得多花时间去全面了解;这篇文章为读者和我自己都开了一个好头。

在日常的编程中,我经常需要标识存在于文本文档中的部件和结构,这些文档包括:日志文件、配置文件、定界的数据以及格式更自由的(但还是半结构化的)报表格式。所有这些文档都拥有它们自己的“小语言”,用于规定什么能够出现在文档内。我编写这些非正式解析任务的程序的方法总是有点象大杂烩,其中包括定制状态机、正则表达式以及上下文驱动的字符串测试。这些程序中的模式大概总是这样:“读一些文本,弄清是否可以用它来做些什么,然后可能再多读一些文本,一直尝试下去。”

解析器将文档中部件和结构的描述提炼成简明、清晰和说明性的规则,确定由什么组成文档。大多数正式的解析器都使用扩展巴科斯范式(Extended Backus-Naur Form,EBNF)上的变体来描述它们所描述的语言的“语法”。基本上,EBNF 语法对您可能在文档中找到的部件赋予名称;另外,较大的部件通常由较小的部件组成。小部件在较大的部件中出现的频率和顺序由操作符指定。

举例来说,清单 1 是 EBNF 语法 typographify.def,我们在 SimpleParse 那篇文章中见到过这个语法(其它工具运行的方式稍有不同):
  

>>> 阅读全文

 

, , , , , ,

手工打造JVM

本文所介绍虚拟机被应用到一个真正的项目中:墨菲斯 – Silverlight 1.1的原型。在本文的附件中的演示讲解了虚拟机是如何工作,你也可以阅读附件中的源代码。不过注意,这儿的实现和任何既有的商业实现都不尽相同。为了节约时间,只要JVM规格书没有详细说明的,我们就采用最简单的方式加以实现。

JVM的各个部分

类文件结构
这个JVM首先就需要个能将各个Java类组合起来的应用,所以谈任何类之前,首先就得定义一个结构“JavaClassFileFormat.”

struct  JavaClassFileFormat
     {
         u4 magic;
         u2 minor_version;
         u2 major_version;
         u2 constant_pool_count;
         cp_info **constant_pool; //[constant_pool_count-1];
         u2 access_flags;
         u2 this_class;
         u2 super_class;
         u2 interfaces_count;
         u2* interfaces; //[interfaces_count];
         u2 fields_count;
         field_info_ex *fields; //[fields_count];
         u2 methods_count;
         method_info_ex* methods; //[methods_count];
         u2 attributes_count;
         attribute_info** attributes; //[attributes_count];
     };

我们看到标有注释的地方,还定义了一些其他的结构,他们用来表示类文件的常量池(类文件中使用的常量)、字段、方法和属性,这儿我们先列出其定义,稍后再详细解释。

struct cp_info
     {
         u1 tag;
         u1* info;
     };
 
     struct field_info
     {
         u2 access_flags;
         u2 name_index;
         u2 descriptor_index;
         u2 attributes_count;
         attribute_info* attributes; //[attributes_count];
     };
     
     struct method_info
     {
         u2 access_flags;
         u2 name_index;
         u2 descriptor_index;
         u2 attributes_count;
         attribute_info* attributes; //[attributes_count];
     };
     
     struct attribute_info
     {
         u2 attribute_name_index;
         u4 attribute_length;
         u1* info;//[attribute_length];
     };

现在要做的是把类文件按字节读到内存中,再用JavaClass对象解析这些原始字节,逐一确定字段、方法、异常表等等。JavaClass类表示一个类在内存中的结构化形式。它有一个指针,指向所加载的类文件的原始字节流。

>>> 阅读全文

 

, ,

.NET编译器构造工具Irony

简介
Irony(不是 IronRuby),是个全新的 .NET 开源项目,其设计目的是为了提供一整套库和工具,以方便在 .NET 平台上实现自己的语言。目前它处于开发的第一阶段,即包括构造编译器的两个前端模块 — 扫描器和解析器。本文将对其技术稍作概述,并着重关注用 Irony 实现解析器。你可以在 CodePlex上找到该项目

Irony 在编译器构造方面引入了许多规则创新。和广泛使用的解析器创建工具类似,Irony 也基于语法规范来产生出可工作的解析器。不同的是,Irony 并不采用独立的元语言来编码目标语法。反之, 在 Irony 中,语法元素被表示为 .NET 对象,并用 C# 使用类 BNF 的表达式对目标语法进行编码。另外,Irony 不会生成额外代码 — Irony 的 LALR 解析器使用源自 C# 语法的编码信息来控制解析过程。

示例
本文所附的下载包中包含了 Irony 的核心装配件、语法浏览器和一系列解析器样例:包括数学表达式的解析器、Schema、Python 和 Ruby 的语法简化版解析器。利用语法浏览器,你可以查看语法案例的分析数据,并利用代码例子来测试解析器。下图是语法浏览器解析语法树的场景:

如果你想体验一下 Irony 项目,你只需要:

  • 1. 下载并解压源代码
  • 2. 用 Visual Studio 打开 Irony_all.sln 方案文件
  • 3. 将 GrammarExplorer 项目设为启动项目
  • 4. 按 F5 创建并运行应用
  • 5. 当语法浏览器的窗口出现的时候,在顶部的组合框内选择”grammer”,并浏览语法数据
  • 6. 从 Irony.Samples\SourceSamples 文件夹中加载源代码示例
  • 7. 解析源代码并浏览输出的语法树

背景
许多新潮的应用都用了这样或者那样的解析技术。比如编译器的开发就是严重依赖于解析器的一个现实例子。其他例子还包括了脚本引擎、表达式计算器(evaluator)、源代码分析和重构工具、基于模板的代码生成、格式化和彩色显示、数据导入和转换工具等等。因此,对于开发者来说,拥有快速直接的方法来实现所需的解析器就很重要了。不过不幸的是,就目前技术而言实现解析器仍然是个重大挑战。

>>> 阅读全文

 

, , , ,

JVM基础概念 之一 JVM基本结构剖析

JVM主要包括两个子系统和两个组件。两个子系统分别是Class loader子系统和Execution engine(执行引擎) 子系统;两个组件分别是Runtime data area (运行时数据区域)组件和Native interface(本地接口)组件。

Class loader子系统的作用:根据给定的全限定名类名(如cc.ejb.example.HelloWorld)来装载class文件的内容到 Runtime data area中的method area(方法区域)。

Execution engine子系统的作用:执行classes中的指令。任何JVM specification实现(JDK)的核心都是Execution engine,不同的厂商有自己不同的实现。

Native interface组件:与native libraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的native heap OutOfMemory。

最后是Runtime Data Area组件:这就是我们常说的JVM的内存了。它主要分为五个部分——

>>> 阅读全文