四月 1

编写 NES 模拟器的难题记录

关于 Bank Switching 的含义

The bank switching technique provides a way for computer systems to access more memory than they would otherwise be capable of. When a computer processor is limited to a specific amount of addressable memory space, additional banks of memory can be set up for the processor to use. These separate banks can then be used to switch away from code that is no longer being used, such as read only memory (ROM) used when starting up the computer, and open up banks of memory for multiple users on the system or store memory for other devices on the system.

Bank switching came about as a cost-effective way to keep computers up and running back in the 1980s without having to replace the processor. It found a good deal of use on older 8-bit computer systems, extending the useful life of a computer by simply adding more memory. As newer systems were developed, they also implemented bank switching methods so that programs created on the older systems could still run.

The way bank switching works is by implementing what’s called a latch technique. The latch is really just something of a switch that toggles the address space that the computer processor is using. For example, 8-bit computers use a 16-bit address space, meaning that they are only capable of working with 64K, or 65,536, individual memory locations at any given time. When a latch was added, either by means of software or hardware, it could then toggle between multiple banks of memory.

The latch is set up separate from the processor, leaving the bank switching in the hands of an external operation. In some cases, it’s simply a bit hiding out in the upper register of memory addresses and toggled as necessary by the computer’s operating system or some other software. As the memory fills, the processor can check the bit at the top and toggle to another bank. Other methods of decoding the latch involved bit-addressable ports that granted access to another bank of memory.

Bank switching found its way into a number of video game consoles from the era as well. The ROM cartridges would come equipped with additional hardware built-in that would expand the console beyond its limited available memory space, allowing for better graphics in games and longer game play through additional stages. As technology and techniques improved, however, the method fell out of use. Some modern operating systems can still emulate bank switching in order to operate older software. Many modern embedded computer systems, those computer systems built into some other device or system and typically designed to perform a single task, also still use bank switching due to its cost effectiveness and ease of use.




std::ifstream in("path/to/file", std::ios::binary);
三月 26

OpenSSL 编译不生成 libeay.lib 和 ssleay.lib 的解决方案

The complete explanation is that 1.0.x and 1.1.x do not have the same naming conventions for the generated libraries. OpenSSL 1.1.x has moved into what they call the “unified build system” and changed themselves the names of the libraries. This was done on purpose, mainly because these libraries are not binary compatible and should not be intermixed into projects or dlls deployed to replace 1.0.x with 1.1.x, and vice versa. So while previously in 1.0.x there were libeay32 and ssleay32, they are in 1.1.x named libssl and libcrypto ( 在1.0.x之前的版本中,文件为libeay32.dll和ssleay32.dll,在1.1.x之后的版本中,名字是libssl.dll和libcrypto.dll ). That’s what happened upstream in OpenSSL. Read here also: https://marc.info/?l=openssl-dev&m=147223063610803&w=2 and there are tons of other discussions online you can tap to.

Beyond that, I also manipulate the suffixes in my builds. Namely, I append the MD[d] and MT[d] suffixes, so that it can be clearer when someone uses a library. This may not be very important when using DLLs, but with static builds chaos ensues if you mix them. So I made my own patches to produce these suffixes to the libraries.

I think that’s a complete answer now. I have also a suggestion for you:

You can download my build scripts if you still like to change the names of the library files in a different way and look at the patch, and modify it accordingly.
You can also skip the application of the patch and then you will get exactly the filenaming conventions of OpenSSL upstream in different builds.

I hope this helps.




Things that Broke in Qt

Here’s what’s broken in the dev branch of Qt when building openssl master as of 6 Feb 2015.

  • DH – we were directly accessing p and q to set the DH params to primes embedded in Qt. We can probably replace this with SSL_CTX_set_dh_auto(ctx, 1). Another option suggested by Steve Henson is to save the DHparams we’re using at the moment then use d2i_DHparams to load them in. This is compatible with openssl versions that don’t have the dh_auto option.
  • ctx->cert_store – we were directly accessing the cert_store field of SSL_CTX. We can probably replace this with X509_STORE *SSL_CTX_get_cert_store(SSL_CTX *ctx) [Fixed in dev]
  • session->tlsext_tick_lifetime_hint – we were directly accessing the lifetime hint of the session. [A new API to access this field has been added]
  • cipher->valid – we were directly accessing the valid field of SSL_CIPHER. No replacement found. [This turned out not to be needed and so will be removed].


三月 14

Modern C++ 语言特性查漏补缺

const 修饰的成员函数


默认构造函数(default constructor)


合成的默认构造函数(synthesized default constructor)

即由编译器创建的默认构造函数(default constructor)

default 修饰的构造函数

若 “= default” 在类内,则此函数是内联的
若 “= default” 在类外,则此函数默认情况下(即没有显式指定 inline)不是内联的

数组指针 & 指针数组 & 返回数组指针的函数

int arr[10];  // 含有 10 个整数的数组
int *p1[10];  // 含有 10 个指针的数组
int (*p2)[10];  // 这是一个指针,指向含有 10 个整数的数组
auto func2(int i) -> int(*)[10];
int (*func(int i))[10];
// func(int i) 表示 func 函数需要 int 类型的实参
// (*func(int i)) 表示该函数的返回值可以被解引用
// (*func(int i))[10] 表示返回值被解引用后是大小是 10 的数组
// 注意这两种别名的区别
typedef int * intp;
typedef int inta[10];

cast 全家桶


用于不包含底层 const,大转小的情况下使用(void * 指针转特定类型指针也可以用)


const 与非 const 之间的互相转换




【格式】:dynamic_cast < type-id > ( expression)

【作用】:将一个基类对象指针(或引用)cast到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理, 即会作出一定的判断。

2、 dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。

#include <iostream>
#include <cassert>
using namespace std;
// 我是父类
class Tfather
	virtual void f() { cout << "father's f()" << endl; }
// 我是子类
class Tson : public Tfather
	void f() { cout << "son's f()" << endl; }
	int data; // 我是子类独有成员
int main()
	Tfather father;
	Tson son;
	son.data = 123;
	Tfather *pf;
	Tson *ps;
	/* 上行转换:没有问题,多态有效 */
	ps = &son;
	pf = dynamic_cast<Tfather *>(ps);
	/* 下行转换(pf实际指向子类对象):没有问题 */
	pf = &son;
	ps = dynamic_cast<Tson *>(pf);
	cout << ps->data << endl;		// 访问子类独有成员有效
	/* 下行转换(pf实际指向父类对象):含有不安全操作,dynamic_cast发挥作用返回NULL */
	pf = &father;
	ps = dynamic_cast<Tson *>(pf);
	assert(ps != NULL);			 	// 违背断言,阻止以下不安全操作
	cout << ps->data << endl;		// 不安全操作,对象实例根本没有data成员
	/* 下行转换(pf实际指向父类对象):含有不安全操作,static_cast无视 */
	pf = &father;
	ps = static_cast<Tson *>(pf);
	assert(ps != NULL);
	cout << ps->data << endl;		// 不安全操作,对象实例根本没有data成员
十二月 17



  1. 数据库管理技术经历了(人工管理阶段)、(文件存储阶段)和(数据库系统)三个阶段。
  2. 数据库管理系统的三级模式是(外模式)、(模式)和(内模式)。
  3. 数据模型通常由(数据结构)、(数据操作)和(数据完整性约束)三部分组成。
  4. 实体间的联系类型分为(一对一)、(一对多)和(多对多)三类
  5. SQL 有两种使用方式:(交互式)和(嵌入式)。
  6. 数据库设计的六个阶段是(需求分析)、(概念结构设计)、(逻辑结构设计)、(物理结构设计)、( 数据库实施 )和(数据库运维)。


  1. 在数据库的三级模式间引入二级映像的主要作用是(提高数据与程序的独立性)。
  2. 视图创建完成后,数据字典中存放的是(视图的定义)。
  3. 在 WHERE 子句的条件表达式中,可以用(%)通配符与所在位置的零个或多个字符相匹配。
  4. 在分组检索中,要去掉不满足条件的分组和不满足条件的记录,应当(先使用 Where 子句,再使用 Having 子句)。
  5. 若采用关系数据库来实现应用,在数据库设计的(逻辑设计阶段)将关系模式进行规范化处理。
  6. 使用 SQL 的 ALTER TABLE 语句修改基本表时,如果要删除其中的某个完整性约束条件,应在语句中使用(DROP)短语。
  7. 用如下的 SQL 语句创建了一个表 S:CREATE TABLE S(S# CHAR(9) NOT NULL, SNAME CHAR(8) NOT NULL, SEX CHAR(2), AGE INT),现向 S 表插入如下行,(\’080502102\’, \’林丹\’, NULL, NULL)可以插入。
  8. 关系模型中系统自动支持的完整性约束是(参照完整性)约束和实体完整性约束。
  9. 已知关系模式 R = { A, B, C, D, E },函数依赖集为 { A -> D, B -> C, E -> A },则该关系模式的候选关键字是()。
  10. 关系模式:学生(学号,课程号,名次),若每一名学生每门课程有一定的名次,每门课程每一名次只有一名学生,则以下叙述中错误的是(关系模式属于 BCNF)
十月 24

自制 MC90 编译系统开发日记


从下决心要自己从零开始写一个 C 语言编译器开始到现在,已经过了一个多月时间了。其实,我作为一名身负着学业的大三普通学生,基本上没有太多的时间完完全全地投入到自制编译器的开发过程中,因为只能在闲暇时构思设计方案以及实现代码,所以开发进展很缓慢,开发周期会比较长。这样的话,编写详细的代码注释和开发日记就很重要,注释能提高代码的可读性,以便于今后长期的开发工作能够可持续地进行下去,而开发日记更像是对自己在开发过程中的思路、遇到的问题进行阶段性的归纳总结。尤其像编译系统这种代码量相比原来大得多的项目,开发日记就显得更加重要。

MC90 编译系统

这个编译器的名字叫做 MC90,MC90 意思就是 “迷你的、兼容 C90 标准的编译器” (Mini C 90)。说到这儿,其实很多朋友可能会建议,可以采用更加个性的、霸气的名字来命名这个 C 语言编译器。我觉得没那个必要,因为我自认为自己水平有限,写出来的编译器配不上某些炫酷霸气的名字,而且我想编写这个编译器的初衷其实是想提高编程能力。比起炫技什么的,开发过程中的乐趣反而更加地吸引我。这份乐趣同样也是支持我学习并实践编程技术的持续动力,它有时可以使我忘记生活中的烦恼不开心的事情,在编程中获得喜悦感和成就感。而 MC90 这朴实无华的名字正合我心意。


既然要设计 C 语言编译器,那么预处理器的设计是首先需要考量的。预处理也正是 C 语言编译系统编译过程的第一步。

到目前为止,mc90 预处理器已经完成了一部分了,主要有三个模块:


行扫描器模块主要由 CombinedLine 类以及 LineScanner 类组成。CombinedLine 是针对 C 语言源程序文本的、以行划分的基本数据结构,一个 CombinedLine 对象包含一个或多个 SingleLine 底层对象(封装采用 std::vector<SingleLine> 实现)。SingleLine 是 std::wstring 类的别名,CombinedLine 描述一行组合行的信息,组合行由一个或多个单行组成,这些单行除最后一行以外结尾都以行延续符 ‘\’ 结尾。

LineScanner 类提供从输入流中扫描并读取组合行的功能,它维护一个可回溯的线性容器,用于存放实际上已经被读取出的、连续的 CombinedLine 对象,其内部采用一个指示器来对底层线性结构进行抽象,并提供组合行扫描过程的回溯和前进的功能。无论如何,LineScanner 类都将保证流中的组合行对象的可回溯性和完整性。

字符扫描器模块主要由 BasicCharacterScanner 类以及 PracticalCharacterScanner 类组成。BasicCharacterScanner 以一个 CombinedLine 对象作为输入,能够扫描并读取其中的字符。这个读取过程被抽象成线性的,并且 BasicCharacterScanner 内部基于 CombinedLine 对象提供的可回溯机制,同样提供了字符的回溯和前进功能。PracticalCharacterScanner 是对 BasicCharacterScanner 的更高级的封装,PracticalCharacterScanner 能有效地过滤掉 CombinedLine 中存在的单行注释和多行注释,并返回除此之外对预处理器和编译器有实际意义的字符。

这些字符最终被封装成 CharacterInfo 数据结构,用于返回字符的值,还有字符所处的行、列的位置信息,便于调用者进行错误定位和汇报。

预处理单词扫描器模块主要由 PreToken 类和 PreTokenScanner 类组成。PreTokenScanner 内部维护一个 PracticalCharacterScanner 对象,通过对 CombinedLine 输入进行字符读取,使用一套特定的有限状态机算法实现由字符到单词的构造功能,这套算法刚好利用了 PracticalCharacterScanner 对象的可回溯机制。扫描器内部也维护了存放 PreToken 对象的线性容器。同样地,PreTokenScanner 提供了可回溯机制,使得上层模块能够在单词流中回溯或前进,这为语法分析提供了基本的数据结构和算法。

PreToken 对象是预处理单词对象,它主要描述了单词的类型和值(单词内容)以及单词的起始行号和起始列号,这些单词最终会被 PreParser 拿去用于构造语法分析树,当然,PreParser 也是通过 PreTokenScanner 来抽取这些 PreToken 对象的。


在开发过程中,我发现如果 PracticalCharacterScanner 对每一个以 CombinedLine 对象来做底层封装的 BasicCharacterScanner 进行消除注释处理,那么这个工作过程将变得十分繁琐。

首先,CombinedLine 由 LineScanner 从 std::ifstream 对象中产生 (这里我定义了别名 InputFileStream,下文都用这个别名来表示字符文件输入流),这时每一个 CombinedLine 对象都是完整的。BasicCharacterScanner 以 CombinedLine 作为输入进行字符读取,而 PracticalCharacterScanner 通过对 BasicCharacterScanner 的封装,调用其读取字符的同时也消除注释。那么,问题就出现了。


如果注释跨越多行,那么 PracticalCharacterScanner 对某一个 CombinedLine 对象的处理显然是不够的,对于跨越多行的注释消除操作,需要一次对多个 CombinedLine 对象进行处理,这样设计就会变得十分复杂。而且,LineScanner 在对 InputFileStream 进行读取并生成 CombinedLine 对象时,CombinedLine 的类型就已经被确定下来(预处理器指令行或者普通程序行),这将直接影响预处理器对此行的处理操作,预处理器只会处理预处理器指令行。那么,如果遇到某种多行注释,刚好延续到某行的预处理器指令之前,那么首先 LineScanner 是不会将此行作为预处理器指令行的,会作为普通程序行进行处理 (LineScanner 会判断一行的首个字符除空格和制表符是否是“#”号,若是,则确定是预处理器指令行,显然,这种情况下,首个非空白的字符不可能是“#”)。

除此之外,延续行符运用在单行注释上的情形,与上述情形类似。如果行注释延续到某一个注释内容的下一行,假设下一行的开头是 “#” 号 (哈哈,当然这种情况很少),按常规情况,这以 “#” 开头的一行也应该被当作注释,可惜,LineScanner “并不知晓” 它是注释行,而且会把此行当作预处理器指令行。


最近我已经想到了解决方法,其实很简单,让 BasicCharacterScanner 直接对 InputFileStream 进行字符读取,PracticalCharacterScanner 基于 BasicCharacterScanner 从 InputFileStream 进行字符读取的同时,使用有限状态机算法,消除行注释和块注释,用空白字符去一一替换原有在注释内容中的字符,除了换行符之外。最后 LineScanner 将负责调用 PracticalCharacterScanner,将根据剔除了注释的字符,处理并生成 CombinedLine 对象,这时结果将是正确的。


武汉轻工大学 2019/11/18

这些天改来改去的,事情也比较多,重构了一些代码,实现了一些工具类和工具函数,最近想着全力将 PreTokenScanner 实现,朝这个方向持续推进下去吧


最近实现了 PpTrace 模块,这在长期来看是有很多好处的,程序具备了底层调试信息的输出功能,将来各个模块的 Debug 将更加容易。经过代码的堆砌,到今天为止,预处理器的词法分析器 (PreTokenScanner) 已经基本上完成了。稳定性方面的话,目前只做了一些简单的测试,PreTokenScanner 运转正常。考虑后续再做一些测试,保证 PreTokenScanner 能够产生相对稳定的单词 (Token) 流,然后就可以基于 PreTokenScanner 来进行预处理语法分析器的编写工作。哈哈,加油!