C 编译器制作入门

附录 3:使用 Docker 搭建开发环境

附录 3:使用 Docker 搭建开发环境

本附录说明一种实用的开发方式:在 macOS 上编辑代码,但把编译和测试放到 Linux 环境中执行。这样既可以继续使用熟悉的编辑器和终端配置,又能保证本书中的汇编、链接和运行结果与 Linux 环境一致。

Docker 提供的是一种轻量级的 Linux 运行环境。

完整虚拟机会模拟一整套硬件,再在其上运行一个普通操作系统;Docker 则更接近于把 Linux 用户态环境封装起来,让程序在隔离的环境中使用 Linux 系统调用。

这个差异使 Docker 的启动速度更快,资源占用也更低。它也常用于服务器部署,因为同一套镜像可以在不同机器上启动出相近的运行环境。

在 macOS 上借助 Docker 开发 Linux 程序,大体有两种做法:

  1. 把 Docker 容器当成一台独立的 Linux 机器,编辑、Git 操作、构建、测试全部在容器里完成。
  2. 编辑代码、提交 Git 等平台无关的工作仍在 macOS 上完成;只有编译、运行测试这类必须依赖 Linux 的命令放到容器中执行。

第一种方式概念简单,等价于另外准备一台 Linux 机器。但它要求你在容器里配置编辑器、Git、Shell 等完整开发环境,准备成本较高。第二种方式不需要“住”在容器里,日常编辑和提交仍在宿主机完成,只在需要 Linux 行为时显式进入容器;缺点是必须理解 Docker 内外两个文件系统如何通过挂载对应起来。

本书采用第二种方式。也就是说,源代码仍保存在宿主机上;需要运行 makemake test 或其他 Linux 命令时,再通过 docker run 把当前项目目录挂载到容器中执行。

搭建步骤

首先安装 Docker Desktop for Mac。安装完成后,执行下面的命令构建本书使用的 Docker 镜像:

$ docker build -t compilerbook https://www.sigbus.info/compilerbook/Dockerfile

镜像可以理解为“容器的模板”:其中包含 Linux 环境所需的文件、工具和配置。由镜像启动出来的运行实例称为 Docker 容器。镜像和容器的关系,近似于可执行文件和进程的关系:可执行文件是静态文件,进程是运行中的实例;镜像是静态模板,容器是运行中的环境。

创建容器并在其中执行命令时,可以使用 docker run。下面的例子在 compilerbook 镜像创建出的容器里执行 ls /

$ docker run --rm compilerbook ls /
bin
boot
dev
etc
home
lib
lib64
proc
root
usr
var

--rm 表示命令执行完后自动删除这个容器。对本书的用法来说,我们通常只需要临时进入 Linux 环境执行一次构建或测试,不需要让容器长期保留。因此每次运行命令时创建一个干净容器,命令结束后销毁它,是最简单的做法。

使用容器构建项目

如果要在容器里执行 make,容器必须能看到宿主机上的源代码。docker run-v 选项可以完成这件事:

-v 宿主机路径:容器内路径

这个选项会把宿主机上的某个目录挂载到容器中。再配合 -w 选项指定容器内的工作目录,就可以像在普通 Linux 目录中一样运行 make

假设你的编译器项目放在宿主机的 $HOME/9cc 目录下,可以用下面的命令在容器中执行测试:

$ docker run --rm -v $HOME/9cc:/9cc -w /9cc compilerbook make test

这条命令的含义是:把宿主机的 $HOME/9cc 挂载到容器里的 /9cc,把当前目录切到 /9cc,然后在 compilerbook 容器中运行 make test。以后构建和测试都可以按这个模式执行。

如果需要进入容器里的交互式 Shell,可以加上 -it

$ docker run --rm -it -v $HOME/9cc:/9cc -w /9cc compilerbook

进入后你会得到一个 Linux Shell,可以手动运行 ccmakeobjdump 等命令。退出时输入 exit 即可。

向镜像中添加程序

上面构建出的镜像已经包含本书需要的一般开发工具,但有时你可能还想安装额外程序。需要注意的是,容器默认是临时运行环境:你在容器里安装的软件,不会自动写回原来的镜像。下一次从同一镜像启动容器时,又会回到原始状态。

如果确实想把容器里的修改保存成新镜像,需要显式使用 docker commit。下面以安装 curl 为例说明。

首先启动一个不带 --rm 的交互式容器:

$ docker run -it compilerbook

注意这里没有加 --rm,否则退出后容器会被自动删除。进入容器后,安装 curl 并退出:

$ sudo apt update
$ sudo apt install -y curl
$ exit

退出后,容器会以停止状态保留在本机。可以用下面的命令查看:

$ docker container ls -a
CONTAINER ID   IMAGE          COMMAND       STATUS                      NAMES
a377e570d1da   compilerbook   "/bin/bash"   Exited (0) 5 seconds ago    pedantic_noyce

其中 a377e570d1da 是容器 ID。用 docker commit 可以把这个停止状态的容器保存回名为 compilerbook 的镜像:

$ docker commit a377e570d1da compilerbook

这样以后从 compilerbook 镜像启动的新容器就会包含刚刚安装的 curl

停止的容器和旧镜像通常只是占用一些磁盘空间,不会影响正在运行的程序。如果想清理这些临时对象,可以执行:

$ docker system prune

该命令会删除未使用的容器、网络、镜像等对象。执行前请确认其中没有你还想保留的内容。