跳转至

2018

近来做 Stanford CS140e 的一些进展和思考(3)

由于 Assignment 2: File System 延期发布,所以中间那段时间转向 MIT 6.828 稍微研究了一下。前几天放出了新的任务,在上一篇文章之后,我又有了一些进展:实现了从内存中读取 ATAGS(ARM Tags) 信息的代码,从而可以获得内存大小的信息,根据这个信息,实现了 bumpbin 两种内存分配器,并且把二者之一注册为全局内存分配器,利用上更新了的 std 就可以使用需要动态分配内存的相关工具了。利用这个,我实现了 shell 输入历史的回溯,把输入历史保存在一个动态增长的数组中,再特殊处理上下键,把当前的行替换为历史。

这个过程也不是没有踩坑。一开始代码放出来了,但是题目说明还没出,我就自己按照代码做了 ATAGSbump 分配器,后来做完了,看到说明出了以后,发现理解还是有偏差,把代码更改了并修复了分配器的 BUG。看到 bin 分配器的时候,我按照网上的 buddy memory allocation 实现了一个内存分配器,原理看起来简单实现起来还是有很多细节问题,后来按照新放出的单元测试,修修补补才写得差不多可用了。同时,原来的 bootloader 因为用了新的 std 而缺失了 alloc 不能编译,我就把 kernel 下的相关文件软连接过去,调了数次后把问题解决。此时, kernel 文件大小已经有 40K,按照 115200 Baudrate 发送需要几秒才能传输过去,我就调到了 230400 Baudrate,果然现在的传输速度就有所提升,可以接受了。等之后写了 EMMC(SD card) 的驱动和 FAT32 的文件系统后,就可以实现更多的 shell 的功能了。中间还遇到一个问题,就是如果给 kernel 开启了 bin 分配器,使用 exit 回到 bootloader 就无法传新的 kernel 上去了,结果发现是因为 bin 中用到的侵入式 LinkedList 实现覆盖了部分 bootloader 的代码,换回不能回收内存的 bump 分配器即可,反正目前远远还用不了那么多内存。

之后还要在 aarch64 上用 MMU 实现虚拟内存,之前在 MIT 6.828 里被页表整得脑子眩晕,希望到时我还活着吧(逃

更新:下一篇在这里

近来做 Stanford CS140e 的一些进展和思考(2)

上一篇文章之后,我又有了一些进展:UART ,简易的shell ,修复了之前写的 xmodem 中的 BUG,一个可以从 UART 接收一个 kernel 写入到内存中再跳转过去的 bootloader

首先是 UART ,就是通过两个 GPIO pin 进行数据传输,首先在 memory mapped IO 上进行相应的初始化,然后包装了 io::Readio::Write (这里实现一开始有 BUG,后来修复了),然后很快地完成了一个仅仅能 echokernel

然后实现了 CONSOLE ,一个对 MiniUart 和单例封装,就可以用 kprint!/kprintln! 宏来输出到 UART ,接着实现了一个 echoshell ,读入一行输出一行。然后实现退格键和方向键,这里的难点在于要控制光标并且用读入的或者空格覆盖掉屏幕上已经显示而不应该显示的内容。接着,利用 skeleton 中的 Command 做了一个简单的 echo 命令。

接着,利用之前编写的 tty ,配合上新编写的 bootloader ,实现通过 UART 把新的 kernel 通过 XMODEM 协议发送到设备,写入 0x80000 启动地址并且调转到新加载的 kernel 中执行。

最后,又实现了 uptime (输出设备启动到现在的时间)和 exit (跳转回 bootloader ,可以上传新的 kernel )。并添加了 TUNA 作为 shell 启动时输出的 BANNER

整个过程挺虐的,踩了很多的坑,由于很多东西都没有,输入输出目前也只有 UART ,写了 UART 后又遇到 XMODEM 难以调试的问题。十分感谢 #tuna 上的 @BenYip 及时地指出了代码的几处问题,节省了我许多时间。

更新:下一篇在这里

近来做 Stanford CS140e 的一些进展和思考

最近,受各路安利,剁手买下了 这个淘宝商家的树莓派的套餐 C ,还买了许多 LED 灯泡、杜邦线和电阻,开始按照 CS 140e 学习 Rust 并且用 Rust 编译写一个简易的操作系统。Assignment 0 的目标就是编写一个向 GPIO 16 连接的 LED 灯闪烁。首先当然就是愉快地按照教程下载 bootloader,下载交叉编译工具链,顺带装一个 Raspbian 到机器上,随时可以当成一个低性能的 ARM/ARM64(实际上,Raspbian 只用了 armv7l,没有用 64bit)机器来用,以后如果配上 @scateu 团购的 Motorola Laptop Dock 的话就是一个几百块的笔记本了。把课程上的文件丢上去,可以看到绿色的活动指示灯闪烁,后面又把 CP2102 模块连上去,又能看到 Blink on, Blink off 的输出。然后按照要求,自己先码一段 C 语言,实现 blinky:

#define GPIO_BASE (0x3F000000 + 0x200000)

volatile unsigned *GPIO_FSEL1 = (volatile unsigned *)(GPIO_BASE + 0x04);
volatile unsigned *GPIO_SET0  = (volatile unsigned *)(GPIO_BASE + 0x1C);
volatile unsigned *GPIO_CLR0  = (volatile unsigned *)(GPIO_BASE + 0x28);

static void spin_sleep_us(unsigned int us) {
  for (unsigned int i = 0; i < us * 6; i++) {
    asm volatile("nop");
  }
}

static void spin_sleep_ms(unsigned int ms) {
  spin_sleep_us(ms * 1000);
}

int main(void) {
  // STEP 1: Set GPIO Pin 16 as output.
  *GPIO_FSEL1 = 0b001 << 18;
  // STEP 2: Continuously set and clear GPIO 16.
  while (1) {
    *GPIO_SET0 = 1 << 16;
    spin_sleep_ms(1000);
    *GPIO_CLR0 = 1 << 16;
    spin_sleep_ms(1000);
  }
}

其中大部分代码都已经给出了,自己要实现也只是查询一下 BCM2837 SoC 的 GPIO 文档,按照文档把该做的内存操作和位运算都写一下即可。最后发现,闪烁的频率特别慢,几秒钟才闪烁一次。毕竟是按照 CPU 的 clock speed 进行粗略的计时,而生成的代码也不是很高效,没有 inline。接着则是用 Rust 再实现一下上面这部分的代码:

#![feature(compiler_builtins_lib, lang_items, asm, pointer_methods)]
#![no_builtins]
#![no_std]

extern crate compiler_builtins;

pub mod lang_items;

const GPIO_BASE: usize = 0x3F000000 + 0x200000;

const GPIO_FSEL1: *mut u32 = (GPIO_BASE + 0x04) as *mut u32;
const GPIO_SET0: *mut u32 = (GPIO_BASE + 0x1C) as *mut u32;
const GPIO_CLR0: *mut u32 = (GPIO_BASE + 0x28) as *mut u32;

#[inline(never)]
fn spin_sleep_ms(ms: usize) {
    for _ in 0..(ms * 600) {
        unsafe { asm!("nop" :::: "volatile"); }
    }
}

#[no_mangle]
pub unsafe extern "C" fn kmain() {
    // STEP 1: Set GPIO Pin 16 as output.
    GPIO_FSEL1.write_volatile(1 << 18);
    // STEP 2: Continuously set and clear GPIO 16.
    loop {
        GPIO_SET0.write_volatile(1 << 16);
        spin_sleep_ms(1000);
        GPIO_CLR0.write_volatile(1 << 16);
        spin_sleep_ms(1000);
    }
}

这边和上面一样,很多东西都已经给出了,只是重新改写一下而已。不过,这边的实测结果则是,一秒钟会闪烁很多下,看了下汇编,生成的循环比较紧凑,所以也没有达到想要的效果,不过后面到我实现了 Timer 的读取之后,就很精准了。

接下来就是痛苦的学习 Rust 的过程,Assignment 1 上来就是解答关于 Rust 语言的一些问题,在过程中被 Rust 十分严格的 Lifetime 和 Borrow checker 弄得想死,好歹最后还是让测试都通过了。接下来就是真正地提供一些封装硬件接口的 API,然后利用这些 API 去实现更多功能,首先是利用栈上分配的空间模拟一个变长数组的 API:stack-vec ,然后是把底层的直接操作硬件的内存操作封装成类型安全的 volatile ,然后实现一个简单的支持断点续传的传文件的协议 xmodem ,又做了一个辅助电脑上使用 TTY+XMODEM 的小工具 ttywrite ,然后就开始撸硬件了:时钟 timer ,针对 GPIO pin 的类型安全的状态机 GPIO 。目前只实现到这里,然后做出了一个准确一秒闪烁的 blinky(令人惊讶的是,因为这里的 kernel 直接从文件头开始就是代码,最后的 binary 异常地小,而之前的代码从文件的偏移 0x8000 开始。目前看来,是因为之前的代码是整个文件加载到 0x0000 上,而代码默认了从 0x8000 开始,所以除了最开头的一个跳转指令,中间留了许多空余的空间。而这里的代码是直接被 bootloader 加载到了 0x80000 处并且跳转到这里执行,所以省去了许多空间):

fn blinky() {
    let mut pin16 = Gpio::new(16);
    let mut pin_out16 = pin16.into_output();

    loop {
        pin_out16.set();
        spin_sleep_ms(1000);
        pin_out16.clear();
        spin_sleep_ms(1000);
    }
}

#[no_mangle]
pub extern "C" fn kmain() {
    // FIXME: Start the shell.
    blinky();
}

目前只做到这里。后面还有大把的坑要踩,难写的 Rust 还得继续啃下去。我的代码都以 diff 的形式放在了 jiegec/cs140e ,写得并不美观。接下来就是实现 UART 了,终于要实现串口通信了。

2018-01-06 更新: 下一篇文章已经更新

再次吐槽 VS 关于 scanf 和 scanf_s 的问题

上次的吐槽后,今天再次遇到同学因为 scanf 在 VS 下的 deprecation error 感到十分迷茫,在知乎上求助又因为拍照的原因被说,我就在此再次吐槽一下 VS 这对初学者很不友善很不友善的两点。

一点就是上面提到的这个,另一点就是程序结束后任意键以退出这一功能要做得更加醒目一点。前者由于大多数新手在学习 C/C++ 的时候都会跟着书上或者网上的代码敲一遍输入输出的代码,很容易就会撞到这个问题。后者则会让新手习惯性地以为程序闪退了,没有出结果,而不知道其实是程序执行结束后关闭而已。

我正在使用的两个 Emacs 的 Patch

我在本地对 emacs.rb 进行了修改:

diff --git a/Formula/emacs.rb b/Formula/emacs.rb
index d0138cd..de3c5ff 100644
--- a/Formula/emacs.rb
+++ b/Formula/emacs.rb
@@ -4,6 +4,14 @@ class Emacs < Formula
   url "https://ftp.gnu.org/gnu/emacs/emacs-25.3.tar.xz"
   sha256 "253ac5e7075e594549b83fd9ec116a9dc37294d415e2f21f8ee109829307c00b"

+  patch do
+    url "https://gist.githubusercontent.com/aatxe/260261daf70865fbf1749095de9172c5/raw/214b50c62450be1cbee9f11cecba846dd66c7d06/patch-multicolor-font.diff"
+  end
+
+  patch do
+    url "https://debbugs.gnu.org/cgi/bugreport.cgi?filename=0001-Fix-child-frame-placement-issues-bug-29953.patch;bug=29953;att=1;msg=8"
+  end
+
   bottle do
     sha256 "d5ce62eb55d64830264873a363a99f3de58c35c0bd1602cb7fd0bc37137b0c9d" => :high_sierra
     sha256 "4d7ff7f96c9812a9f58cd45796aef789a1b5d26c58e3e68ecf520fab34af524d" => :sierra

主要涉及到两个 Patch:

  1. 启用对 Multicolor font,比如 Emoji 的支持。由于一些 ethic problems 暂时在 Emacs 中被禁用了,所以自己启用回来。
  2. 打上我前几天上报的 BUG #29953 的修复。已经在上游 Merge 到 emacs-26 分支中,这个修复会在下一个版本中。

有了第一个,就可以正常显示 Emoji(对不起,RMS);有了第二个,就解决了 pyimlsp-ui-peekchild-frame 显示的一些问题了。

另外还有一个我自己在用的 recoll.rb

# Documentation: https://docs.brew.sh/Formula-Cookbook.html
#                http://www.rubydoc.info/github/Homebrew/brew/master/Formula
# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!

class Recoll < Formula
  desc "Recoll is a desktop full-text search tool."
  homepage "https://www.lesbonscomptes.com/recoll/"
  url "https://www.lesbonscomptes.com/recoll/recoll-1.23.5.tar.gz"
  sha256 "9b6b6941efc3e87c8325e95a69a5d0a37c022c3c45773c71dccd0fb3f364475f"

  depends_on "xapian"
  depends_on "qt"
  depends_on "aspell"

  def install
    inreplace "Makefile.in",
      "-Wl,--no-undefined -Wl,--warn-unresolved-symbols", "--no-undefined --warn-unresolved-symbols"

    system "./configure", "--disable-dependency-tracking",
                          "--disable-silent-rules",
                          "--without-x",
                          "--disable-x11mon",
                          "--with-aspell",
                          "--enable-recollq",
                          "--disable-webkit", # requires qtwebkit, which is not bundled with qt5
                          "--prefix=#{prefix}"
    system "make", "install"

    mkdir libexec
    mv bin/"recoll.app", libexec/"recoll.app"
  end

  test do
    # `test do` will create, run in and delete a temporary directory.
    #
    # This test will fail and we won't accept that! For Homebrew/homebrew-core
    # this will need to be a test that verifies the functionality of the
    # software. Run the test with `brew test recoll`. Options passed
    # to `brew install` such as `--HEAD` also need to be provided to `brew test`.
    #
    # The installed folder is not in the path, so use the entire path to any
    # executables being tested: `system "#{bin}/program", "do", "something"`.
    system "false"
  end
end

NAT64 初尝试

最近宿舍里有线网络的 IPv4 总是拿不到地址,只能连无线网,不禁对计算机系学生的可怕的设备数量有了深刻的认识。不过,作为一个有道德(误)的良好青年,还是不要给已经枯竭的 IPv4 地址填堵了,还是赶紧玩玩 IPv6 的网络吧。然后在 TUNA 群里受青年千人续本达 (@heroxbd) 的安利,本地搭建一下 NAT64+DNS64 的环境。不过考虑到宿舍还是拿不到有线的 IPv4 地址,我就先利用苹果先前在强制 iOS 的应用支持 NAT64 网络的同时,在 macOS 上为了方便开发者调试,提供的便捷的建立 NAT64 网络的能力。

首先在设置中按住 Option 键打开 Sharing,点击 Internet Sharing,勾上 Create NAT64 Network 然后把网络共享给设备。然后在手机上关掉 Wi-Fi 和 Cellular,发现还能正常上网。此时可以打开 Wireshark 验证我们的成果了:

在手机上打开浏览器,浏览千度,得到如下的 Wireshark 截图: baidu-nat64

这里,2001:2:0:aab1::1 是本机在这个子网中的地址,2001:2::aab1:cda2:5de:87f6:fd78 是我的 iOS 设备的地址,然后 iOS 向 macOS 发出了 DNS 请求,macOS 发送 DNS 请求后得到 baidu.com 的 IPv4 地址之一为 111.13.101.208baidu-dns

上图中,我们可以看到, baidu.comAAAA 记录是 2001:2:0:1baa::6f0d:65d0 ,这个就是 DNS64 转译的地址,前面为网关的 prefix ,后面就是对应的 IPv4 地址: 0x6f=111, 0x0d=13, 0x65=101, 0xd0=208 ,当客户端向这个地址发包的时候,网关发现前缀符合条件,把最后的这部分 IPv4 地址取出来,自己把包发送到真实的地址上去,再把返回来的包再转为 IPv6 的地址返还给客户端。可以验证,剩下的几个地址也符合这个转译规则。

这就实现了:利用一台连接着 IPv6 和 IPv4 两种网络的网关,可以使得 IPv6 这个网络通过网关访问 IPv4。通过配置,也可以使得 IPv4 访问 IPv6 中的地址(即 Stateful 和 Stateless 的区分,需要手动配置映射)。

好处:作为比较成熟的 IPv4 到 IPv6 过渡方案之一,可以让自己组建的 IPv6 网络访问一些仅 IPv4 的网站。 坏处:依赖于 DNS64,必须要经过一层翻译,一些应用或协议可能写死了 IPv4 的地址,该方法可能会失效。