本篇采用倒叙法,将废话放在片尾

原因:JavaScript在浏览器中顺序而异步地执行,导致某些需要有前后顺序要求的操作失效。比如先要通过XHR获得token,再用token访问新网页,异步执行会导致访问新网页的时候用的token可能还是undefined,即此时XHR请求还没完成。

解决方案:Promise对象

参考了廖雪峰老师的博客:https://www.liaoxuefeng.com/wiki/1022910821149312/1023024413276544

Promise的使用,在下将其归类为两种操作,创建和运行。一种创建Promise对象,另一种对Promise对象进行操作:进入下一状态或者得出结果。

Promise对象的创建:两种方法,一个是直接new,另一个利用Promise.resolve()或者Promise.reject()是创建有状态的Promise。后者两者分别对应一个Promise对象在执行成功和出错时的返回结果。从语法上允许直接向上述两个Promise方法传递一个Object,作为一个新Promise的返回结果,但是从使用上一般都是作为管道,即将结果从一个Promise传递到另一个Promise,不会用于一个全新Promise的生成。

将操作写入到Promise对象的函数中,然后就可以“运行”该Promise对象。Promise对象有一个then()方法,该方法接受一个函数作为参数,执行的工作是将传入Promise的函数体执行后,将(如果存在的)resolve的参数值作为then()方法内传入的函数的第一个参数,然后执行。结合代码解释如下:

function addBy1(a) {
  return new Promise((resolv, reject) => {
    resolv(a + 1);
  })
}

var p = addBy1(0);

首先在此创建了一个函数,命名为addBy1,接受一个参数,结合函数体可以大致猜出,该函数的功能是将传入a的值加1。

Promise对象有两种自用的返回值的方法:resolve()reject(),可以分别理解为带成功和失败状态的返回结果。在直接使用new关键字生成一个新的Promise对象时,这两个函数以参数的形式传入函数体,供不同条件下的返回使用。

实际上,可以直接将addBy1的return语句中的部分用于变量p的赋值。但是为何在这里分开呢?下面自有用处。(突然跑题)

显然,若是将addBy1函数就视作一个将传入数字加1的功能的话,应该得到相应的结果。如何得到呢?答案是使用Promise对象的运行功能。(对=>运算符略作解释:该符号用于简化匿名函数的表达,(参数列表)=>{函数体}

p.then(
  (a) => {
    console.log(a);
  }
);

然后就可以获得梦寐以求的1了。但是,显然功能不会就这么简单。Promise的妙处就在于这个then(),简要来说,它提供了在异步执行的JavaScript中执行一系列的同步操作的能力。正是有了这个功能,使得需要依赖前一个操作完成后再执行下一个操作的逻辑流程能够按照正确的时序图执行。直观地感受一下:

var p = addBy1(0);
p.then(addBy1).then(addBy1).then(addBy1).then(addBy1).then(
  (a) => {
    console.log(a);
  }
);

在执行了这一段代码后,会发现输出的结果是5:从创建变量开始,一共执行了5次addBy1函数,而初始值为0。

但是若是再使用上面的一段代码输出p中的返回值,会发现其仍然是1。理解为then()方法不会改变原始Promise对象的状态吧。

then()函数接收的参数若是返回Promise对象,就可以在后面继续接.then(),从而可以定义无限长的.then()序列。而这些then()方法又必须串行执行。这就达到了一开始的目的:串行地执行JavaScript代码。

这么一说,操作都是n+1,怎么看得出来到底是不是1+1,2+1一直到4+1呢?虽然从逻辑上似乎也找不出其他的可行执行方法,但是可以通过在第一段代码的resolv()函数之前加一句console.log(a);来验证,显然输出结果会是01234。因而拓展一下,将简单的1+1操作换为需要较长时间的,比如XHR请求一类的操作的话,就可以方便地实现时序的可控化了。

最后,上文不是有提到为什么要将addBy1操作单独提出作为一个函数吗。原因有二,其一就是上文已经用到了的,为了重复进行Promise的操作,其二就是,这是一个极好的函数式编程的入门范例。函数式编程的思想之一就是使用尽可能少的参数。比如传统的1+1会表示为var result = plus(1, 1),而函数式编程会这么想:

class Number {
  constructor(x) {this.number = x;}
  addBy1() {
    return this.number + 1;
  }
}

即在正常使用时,会这么用:

var a = new Number(1);
var result = a.addBy1();

从而得到2。在使用过程中,除开对象的声明之外,没有用到一个有参数的方法。


最近处于各种原因,开始了前端编程。。然后上手就是微信(诶,好像不对)丁丁(也不对)钉钉(啊对,就是这个了)小程序。不出所料,工作就是生物代码粘合器(2077警告)。这里即兴赋歌一段:

我有个appId 我有个appSecret (双手做插合状,并摆出夸张的面部表情) access_token

我有个access_token 我有个authCode (双手做插合状,并摆出夸张的面部表情) userId

(大雾)

虽然对于很多干这行的人来说,可能认为在下这是对工作本身的不尊重,但是这确实反映了在下在看文档的时候,看到为了获取一个userId而不得不从appIdappSecret开始一路以token换token最终拿到所需要的数据的过程中,哭笑不得的感觉。我当时不选择做前端,大概就有一部分原因在这。虽然知道出于安全角度考虑,这是较为稳妥的一种方式,但是如果深陷其中,并以其为日常的工作,显然就太有些小看程序员的能力了。(咳咳,但是砖总得要有人去搬啊)

https://www.cnblogs.com/xiami-xm/articles/8417526.html

由于GEM5需要从原始的Linux内核可执行文件启动,不能使用压缩过的vmlinuz,所以一开始的想法是从当前发行版的软件包管理器下载压缩过的内核,然后从里面解压出完整版本的来。在Stackoverflow上看到了两种方法:

  1. 二进制暴力解法 https://superuser.com/questions/298826/how-do-i-uncompress-vmlinuz-to-vmlinux 分析vmlinuz的构成原理,可以得知其拆解方式。在此仅引用结论,原理学习还请暂时移步其他博客。 zImagebzImage都是指的使用gzip压缩的内核,有无b的主要区别在于内核文件的大小,实际上b就代表big。作为第一个在裸处理器上运行的程序,内核显然不能指望有其他程序能够替自己解压缩,因而vmlinuz是包含有gzip自解压的代码的。也就是说,vmlinuz文件至少由压缩的内核和预操作(比如解压)代码组成。在承认这些部分的数据是各自连续的情况下,可以通过在其中寻找gzip的文件头,来寻找压缩内核的位置,然后解压即可获得原始内核。

借用原文中的例子:

> od -A d -t x1 vmlinuz | grep '1f 8b 08 00'
0024576 24 26 27 00 ae 21 16 00 1f 8b 08 00 7f 2f 6b 45

其中1f 8b 08 00是gzip文件的文件头,标识一个合法的gzip文件开头。 很幸运,在当前的vmlinuz中似乎还是只含有一个gzip文件,而这个文件正是我们所需要的,经过gzip压缩的vmlinux。

> dd if=vmlinuz bs=1 skip=24584 | zcat > vmlinux
1450414+0 records in
1450414+0 records out
1450414 bytes (1.5 MB) copied, 6.78127 s, 214 kB/s

使用dd命令,从指定位置复制数据出来,bs=1表示每次的数据操作量为1字节,使用这个值能够较为简便地确定开始位置(od命令所提供的偏移量就是以字节为单位的)。比如上文中的0024576表示十六进制数据“24”相对文件首部的偏移量为24576 * 1字节 = 24576字节。将偏移量定位到gzip文件首部的“1f”,相对“24”向后偏移了8字节,故dd中的命令偏移量设置为24576 + 8 = 24584。使用管道将二进制内容直接发送到zcat,在命令行内完成文件的解压,得到预期中的vmlinux文件。

  1. 使用Linux内核包提供的解压工具(天知道原理和1是不是一样的) https://stackoverflow.com/questions/12002315/extract-vmlinux-from-vmlinuz-or-bzimage 简单说就是使用scripts目录下的extract-vmlinux脚本来完成操作。不过这个我没试过。

几乎完全参考自:https://blog.csdn.net/u012822903/article/details/64506037

  1. 下载所需系统的Minimal(最小版本),或者Cloud Image(云镜像),不用安装的那种

  2. 使用util目录下的gem5img.py生成镜像:python2 util/gem5img.py init /path/to/your/img <size> 其中size表示镜像大小,一般单位为MB。根据原文,使用了root权限建立镜像。

  3. 挂载镜像: 先用losetup将文件挂载为系统可识别的设备,然后再用mount命令挂载为磁盘分区。 gem5img.py可以自动完成这一系列工作:sudo util/gem5img.py mount /path/to/your/img /mount/folder

  4. 把Linux系统上的文件树复制过去,如果是压缩文件的话直接解压到根目录即可

  5. 设置串口作为GEM5与虚拟系统的默认通信方式: 建立etc/init/tty-gem5.conf

    # ttyS0 - getty
    #
    #This service maintains a getty on ttyS0 from the point the system is
    # started until it is shut down again, unless there is a script passed to gem5.
    # If there is a script, the script is executed then simulation is stopped.
    
    start on stopped rc RUNLEVEL=[12345]
    stop on runlevel [!12345]
    
    console owner
    respawn
    script
       # Create the serial tty if it doesn't already exist
       if [ ! -c /dev/ttyS0  ]
       then
         mknod /dev/ttyS0 -m 660 /dev/ttyS0 c 4 64
       fi
    
       # Try to read in the script from the host system
       /sbin/m5 readfile > /tmp/script
       chmod 755 /tmp/script
       if [ -s /tmp/script  ]
       then
         # If there is a script, execute the script and then exit the simulation
         exec su root -c '/tmp/script' # gives script full privileges as root user in multi-user mode
         /sbin/m5 exit
       else
         # If there is no script, login the root user and drop to a console
         # Use m5term to connect to this console
         exec /sbin/getty --autologin root -8 38400 ttyS0
       fi
    
    end script
  6. 按照目标架构,编译好m5文件放到sbin目录下: 由于不同架构下的编译方式可能不一样,make的时候要用-f选项指定目标平台所对应的Makefile。

  7. 系统内安装应用 不知道为啥要链接三个目录,但是这里不是用ln,因为非系统底层的文件系统映射,并不是所有软件都能较好支持。 这里用mount的bind选项:

    sudo /bin/mount -o bind /dev dev
    sudo /bin/mount -o bind /sys sys
    sudo /bin/mount -o bind /proc proc

    然后用chroot来模拟对另一套文件系统的操作:sudo chroot /path/to/mounted/root /bin/bash 这时的bash就会按照新的伪根目录进行操作了,有root权限。内核还是宿主系统的,显然。 操作完成之后要卸载镜像,在此之前需要先把上面的三个目录umount一下。

  8. 关于启动 最小版本的Ubuntu内部是不含有启动内核的。一般来说,内核文件是放在/boot目录下的。Ubuntu的最小版本可以通过apt下载对应的内核,但是这个方法并不适用于GEM5。GEM5的启动方式是直接执行内核,因而内核必须是一个完整的文件,静态编译且保留符号才可运行,所以在镜像外部准备一个未经压缩的完整Linux内核,在启动时指定为参数即可。现在发行版提供的大多是vmlinuz,是将原始版本vmlinux脱去符号、再经gzip压缩后的版本,还原后由于没有符号不能用于GEM5,还原方法打算另开一文简述(已经写好了->参见这里)。所以考虑一下自己编译吧。

一个单文件的C语言源程序也是一个C项目。


在之前用C语言编程的时候,往往都是随意地新建一个.c.cpp文件,把代码往里面一写,gcc命令一敲,然后就在命令行里运行自己的程序,开始快乐苦B的debug之旅了。直到最近学习编译原理,了解了一些程序解析的原理之后,才开始把这个曾经思考过,却又未曾放在心上的问题拿出来解决。

在看了这篇文章之后,有了一定的新认识,决定也自己写点东西来加深一下理解。(看起来是位高中物竞,大学计科的dalao啊)

首先提出一个问题:C语言中的.c.h读者有见到过吗?在使用时,有注意到自己是在何种情况下使用两者的吗?

显然,一个接触过C语言的人,是一定见到过这两者的。因为,即使是一个编程界最基础的程序Hello World,在使用C语言编写时也必须同时用到这两种类型的文件。

回忆一下,步骤如下:

  1. 建立helloworld.c
  2. 使用任意文本编辑器(如记事本)打开,在内部输入如下内容:

    #include <stdio.h>
    int main()
    {
    printf("Hello world!\n");
    return 0;
    }
  3. Ctrl+S,关闭;
  4. 使用编译器编译(如GCC):gcc -o helloworld.exe helloworld.c
  5. 运行:helloworld.exe,或者./helloworld.exe

于是我们就见到了helloworld.cstdio.h两个文件。至少到这里,一般人都会有的想法:编写自己的程序用.c后缀,使用系统预定义的函数用.h后缀。为什么不能反过来呢?理论上可以。不过由于这已经形成了约定俗成的习惯,随意地违反规则,可能造成意料之外的结果。

接下来,按照大众称呼,将.c文件称呼为源文件source file.h文件称呼为头文件header file。何为源文件呢?程序是由代码编译生成的,代码是程序之源,因而代码又叫做程序的源代码;源代码中的文件自然而然地便可以称为源文件了。那何为头文件呢?浅显易懂的理解方法是,.h文件作为include预处理指令的操作对象,一般都是随预处理指令放在文件头部位置的。(C语言中代码功能与书写位置是有关的;作用域为定义位置开始向后,所以需要的功能最好在一开始就引用。

在上文所提到的文章中,作者表示,由于include预编译指令的实际作用是将目标文件直接复制到当前文件的同一位置,所以文件到底叫啥,有什么后缀名都无关紧要,因为文件名在include预编译指令中必须明确指定。一开始我还在这踩了个坑,以为头文件中函数的实现只能在名称对应的源文件中查找,直到我试了一下。。。发现实际上至少在Visual Studio中,只要是在解决方案资源管理器中添加了的文件,都算是源文件,即使名字不对应,后缀奇怪,也不会影响函数实现的搜索,平时常见的头文件和源文件命名对应的目的只是为了方便开发人员寻找函数的实现代码。换个说法,就是MSVC会在编译时把添加到项目资源管理器的文件都扫描一遍,生成对应的目标文件(符号表)。即使后缀改成exe,只要没从解决方案中排除,照样编译给你看

寻找的两种不同的库文件(Windows下的.dll,Linux下的.so;Windows下的.lib,Linux下的.a),依次对应的是动态编译和静态编译两种各有优劣的编译方法。在此不做展开,关于这个内容的博客在搜索引擎上还是很好找的。由于了解尚不够深入,目前还不清楚源文件的有无是否影响动态编译——反正静态是肯定不影响的:编译器往往提供了其C标准实现(有多种,如GCC的glibc,BSD的BSD libc,Windows平台下的MSVC Runtime等等)的静态编译库,而不提供其实现源码。(从某种程度上来讲实现了对源码的保护)

但是在最简单的Hello World例子中,我们是无法找到stdio.c这个文件的。因为往往编译器都提供的是静态库,如上一段所说,原因为何我也不清楚。。

由于Windows下的MinGW采用的C标准实现是MSVC的,因而在编译程序时,所需要的函数实现都是从MSVC对应的静态库里提取的。这个静态库一般是libmsvcrt.a,使用strings工具可以看出其中存在printf字符串,可以间接地表明其中含有printf的实现。顺便了解到Windows下与grep对应的命令是findstr

由include的原理所牵扯到的是,“所include的文件一定要是头文件,而不能是源文件吗?”这样一个问题。这个想法很好:把源文件复制到待引用文件的头部的话,相当于是只是把函数的定义换了个位置写,然后用编译器命令复制过去,在理论上是行得通的。然而若是亲自实践的话,会发现编译器报出符号多重定义的错误,即实际上并不可行。

这个思考方式忽视了一个问题:打开任何一个现有项目的的头文件查看,里面都是只有函数的原型声明的,没有任何实现代码。上面也有提到,编译器会自动到所有指定为源文件的文件中寻找函数的实现,来试图在链接过程使这个函数具体化。那么如何实现寻找呢?在对源码直接进行搜索的基础上进行改进,把对源文件的词法和语法分析的中间结果记录下来,也就是可能会听说过的.obj文件,里面就有编译器可以识别格式的文件信息,包括定义的变量、函数等。再把函数名字相同的函数寻找出来(由于C语言是面向过程型的语言,没有重载特性,故没有函数签名一说),作为函数声明的实现。也就是说,可以理解为,编译期先将源文件解析为一一对应的一系列“符号集合”,然后在“符号集合”的并集里寻找匹配的函数实现。那么,上面的问题就好解决了:如果include了一个源文件的话,声明引用的文件中会出现被引用文件的代码,这些函数被定义了一遍,放进该文件的符号表里;同时由于被引用的文件被视为了源文件,该文件自身也会有一个对应的符号表,把文件里的函数定义一遍。由于这个“并集”并不会、且出于严谨的目的也不能自动合并命名重复的函数实现,因而在链接阶段,链接器将不知道该采用哪个实现,直接报出多重定义的错误。

如果我不把被include的文件添加至项目呢?

这个更好回答,你告诉编译器这个不是源代码文件,编译器就会直接在词法分析预处理的阶段报错。需要的文件“不存在”(因为你告诉编译器不是这个文件,而它又找不到其他满足条件的文件),词法分析无法进行。

而函数的声明和定义就不一样了,声明是针对编译的过程而言的,为的是向语法分析器确认该函数的用法。至于该函数到底有啥用,语法分析器不管,因为它不需要关心自己所处理的句子的内在含义的问题,只要句子“读得通”(符合文法)就行。所以,函数的声明是不需要写入到符号表的,相同的声明可以在不同的源文件中同时出现。这个时候,头文件的作用就显现出来了:能够规范简洁而统一地对需要引用的同一(批)函数进行声明。这个功能还可以扩展至所有声明性的工作,包括宏定义#define、C++的类定义、结构体以及枚举类型的定义,等等。但凡涉及到实体化的工作,即使是变量声明,也不能放在头文件中,因为只要有实体的对象都会被添加至符号表,造成重定义的错误。

总结如下: .c文件里写代码(放实体),.h文件里放声明(说空话)。因为口说无凭,立字为据嘛

  1. 优先级去除文法的二义性 优先级在于确定无二义的文法时有重大作用,它可以确定在具有多个可选右侧的产生式中,到底选择哪个的顺序。显然这个顺序在产生式本身是无法定义的,所以需要通过多级的产生式来强行确立一个顺序:必须先完成这个表达式的匹配,才能进行下一级匹配。 由于个人的逻辑水平实在有限,发现可以作为一个规律记忆的是,最先匹配的产生式右侧,在运算时的优先级是最低的(出现多个优先级相同的运算符的话,就需要规定推导方式了,比如最左?)。也就是说,需要最先计算的表达式,是需要最后匹配的。至于为什么是这样。。暂时不知道

  2. 不连续的模式句型 比如1n0m1m0n,两侧的1和0出现次数一致,中间的次数也一致。这个时候中间部分照常处理,用0A1替换即可,外侧的不连续0和1则可以视作附在A两侧的对称句型,与A不同的是对称中心不为空而是A。也就是说,外侧句型B中间不是10而是1A0,这样就解决了中间夹了一个A的问题,即B ::= 1B0 | A。实际上A就是B中对称中心为空的特殊情况。之前被老师坑了,以为产生式右侧不能放ε,所以就对右侧有了奇怪的理解,比如A ::= 0A1 | 01。。。