Zig 編譯器的開發調試流程

最近一段時間給 Zig 編譯器解決了幾個 Bug,基本上把 Zig 編譯器的開發調試流程給掌握了。因爲 Zig 編譯器開發調試的相關文檔很少,自己也在剛開始時也碰到了些問題,花了點時間摸索。因此整理了一篇文檔,分享出來,給有興趣給 Zig 編譯器修復 Bug、增加特性的軟件工程師們做參考。

Zig 編譯器主要是由 Zig 語言寫的,現在已經完成了自舉。目前使用的後端有 LLVM,以及 Zig 語言的自託管後端。所以 Zig 編譯器目前還需要依賴 clang、llvm 和 lld 這幾個軟件包,其他依賴就沒有了。

我使用的操作系統是 macOS 15.3,所以我在下面以這個系統爲運行環境來介紹 Zig 編譯器的開發調試流程。其他的系統比如 Linux 和 Windows 的開發流程是類似的,不過一般推薦 Linux 和 macOS,Windows 系統遇到的問題會相對多一些。

首先,需要安裝當前最新版本的 Zig 編譯器,並確保可以正常運行 Zig 編譯器,這樣 Zig 編譯器依賴的 clang、llvm 和 lld 這幾個軟件包就已經安裝好了。比如,現在 Zig 編譯的最新版本是 0.14,就安裝這個版本的編譯器。

接着,從 github 上下載 Zig 編譯器的源代碼,具體命令如下:

git clone https://github.com/ziglang/zig.git

建立一個你自己的 0.14 版本的開發分支,具體命令如下:

git checkout -b dev 0.14.0

這樣,源代碼環境就準備好了,你就可以在這個分支的代碼上加上你的修改來修復 Bug、增加新特性了。

爲了後續方便編譯源代碼,以及正確配置依賴的軟件包,需要先完全編譯一次 Zig 編譯器的源代碼,也就是使用 bootstrap 的模式來編譯。先進入 Zig 編譯器的源代碼目錄,然後在 macOS 下使用如下具體命令:

mkdir -p build  &&  cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$(brew --prefix llvm@19);$(brew --prefix lld);$(brew --prefix zstd)" -DZIG_STATIC_ZSTD=ON -DZIG_STATIC_LLVM=OFF -DZIG_SHARED_LLVM=ON
make -j10

上面的 make -j10 命令中,10 這個數字需要根據你自己的 CPU 的核心數來調整,大致和核心數相當即可。這個編譯過程會比較長,編譯成功後會有一個 stage3 目錄,這個目錄下就是新編譯出的編譯器和對應的標準庫。

這個準備工作做完後,就可以開始修復 Zig 編譯器的 Bug 或者添加新特性了。下面以一個簡單的 Bug 的修復爲例來講解具體的過程。

在 Zig 項目的 issue 裏查找待解決的 Bug,比如這個 Bug:https://github.com/ziglang/zig/issues/21202。這個 Bug 提的很有水平,有短小簡潔的復現 Bug 的例子代碼,同時有 Bug 發生時 Zig 編譯器 crash 的調用棧信息和對應的 Zir 代碼。這對快速定位 Bug 非常有用。

現在就可以開始來解決這個 Bug 了,首先,使用如下的 git 命令建立一個 fix-reinterp-0-sized-array 的分支,在這個分支上來修復 Bug。

git checkout -b fix-reinterp-0-sized-array 0.14.0

因爲 Zig 編譯器的完全編譯非常慢,所以 Zig 還有一種更快的編譯方式,就是使用當前的 Zig 編譯器通過 zig build 命令來編譯 Zig 編譯器。當前情況下就是使用 Zig 0.14 版本的編譯器來編譯 0.14 版本的 Zig 源代碼,但直接這樣做會有源代碼版本不夠新的問題,需要使用下面的 patch 來將 Zig 源代碼的版本提升爲 0.14.1,這樣纔可以使用 zig build 命令來編譯。

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 700b724b00..569e8f4c45 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -39,7 +39,7 @@ project(zig
​
 set(ZIG_VERSION_MAJOR 0)
 set(ZIG_VERSION_MINOR 14)
-set(ZIG_VERSION_PATCH 0)
+set(ZIG_VERSION_PATCH 1)
 set(ZIG_VERSION "" CACHE STRING "Override Zig version string. Default is to find out with git.")if("${ZIG_VERSION}" STREQUAL "")
diff --git a/build.zig b/build.zig
index 2d196041b3..59dbaa703f 100644
--- a/build.zig
+++ b/build.zig
@@ -11,7 +11,7 @@ const assert = std.debug.assert;
 const DevEnv = @import("src/dev.zig").Env;
 const ValueInterpretMode = enum { direct, by_name };
​
-const zig_version: std.SemanticVersion = .{ .major = 0, .minor = 14, .patch = 0 };
+const zig_version: std.SemanticVersion = .{ .major = 0, .minor = 14, .patch = 1 };
 const stack_size = 46 * 1024 * 1024;
​
 pub fn build(b: *std.Build) !void {

更新完這個 patch 後,使用如下的 zig build 命令來編譯 Zig 編譯器,具體命令如下:

zig build \
    -Dlog \
    -Denable-llvm \
    -Dconfig_h=build/config.h \
    -Dno-langref \
    --zig-lib-dir lib

使用上面的命令編譯出來的是 Debug 版的 Zig 編譯器,編譯 Release 版本的編譯器加上 - Doptimize=ReleaseFast 參數即可。這個編譯過程將會快很多,大致一兩分鐘即可完成。編譯成功後,生成的編譯器和標準庫保存在 zig-out 目錄下,其中 zig-out/bin/zig 這個可執行文件就是生成的 Zig 編譯器。如果你編譯的是 Release 版本,則可以直接將 zig-out 目錄拷貝到你常用的軟件安裝目錄,比如 / opt,並將 zig-out 改名爲 zig-0.14.1,就完成了安裝,非常簡單。以後使用 / opt/zig-0.14.1/bin/zig build 就可以使用你新生成的編譯器來編譯 Zig 項目了,當然,你也可以設置 shell 的 PATH 變量,將 / opt/zig-0.14.1/bin / 加入到 PATH 變量中,這樣你就可以替代原先安裝的 Zig 編譯器了。

我們來看一下這個 Bug 的報告信息,有如下的錯誤和相關的調用棧:

thread 32651 panic: index out of bounds: index 0, len 0
Analyzing a.zig
      ...
      %14 = block_comptime({
        %15 = ptr_type(@void_type, One) node_offset:5:20 to :5:25
        %16 = break(%14, %15)
      }) node_offset:5:20 to :5:25
      %17 = dbg_stmt(3, 28)
      %18 = ptr_cast(%14, %10) node_offset:5:28 to :5:45
      %19 = as_node(%14, %18) node_offset:5:28 to :5:45
      ...
    > %26 = store_node(%19, @void_value) node_offset:6:5 to :6:19
      ...
 
/home/vexu/Documents/zig/zig/src/mutable_value.zig:333:51: 0xb715ab3 in elem (zig)
            .aggregate => |*agg| return &agg.elems[field_idx],
                                                  ^
/home/vexu/Documents/zig/zig/src/Sema/comptime_ptr_access.zig:864:47: 0xb719573 in prepareComptimePtrStore (zig)
                    cur_val = try cur_val.elem(pt, sema.arena, @intCast(elem_idx));
                                              ^
/home/vexu/Documents/zig/zig/src/Sema/comptime_ptr_access.zig:88:46: 0xb722ce8 in storeComptimePtr (zig)
    const strat = try prepareComptimePtrStore(sema, block, src, ptr, pseudo_store_ty, 0);
                                             ^
...

從 Zir 的信息中可以看到,錯誤發生在將一個 void 類型的值保存到一個指向 void 類型的指針中。從調用棧來看是在計算具體的指針地址時發生了錯誤。這是發生在 Sema 階段的一個錯誤。

我們詳細分析錯誤發生時的代碼,可以看到,因爲我們要保存 void 類型的值到一個指向 void 類型的指針中,所以此時 agg.elems 這個數組的長度是 0,這個時候就不應該對這個數組做索引操作,也就是不應該有如下代碼的調用:

cur_val = try cur_val.elem(pt, sema.arena, @intCast(elem_idx));

往上查找調用棧,可以看到是在函數 storeComptimePtr 中調用了函數 prepareComptimePtrStore,函數 prepareComptimePtrStore 調用了函數 elem,從而引發的上述調用。接下來我們就分析一下 storeComptimePtr 這個函數吧。可以看到這個函數在調用 prepareComptimePtrStore 前面就是檢查了一下 host_bits 和 bit_offset,都是必要的檢查,那如何避免調用函數 prepareComptimePtrStore,從而引發 Bug 呢?

因爲是要保存 void 類型的值到一個指向 void 類型的指針中引發的 Bug,而實際上這個保存 void 類型值的動作是不會有任何實際的作用的,是一個空操作。所以可以在函數 storeComptimePtr 一開始就判斷要保存的指針類型,如果是指向 zero-bit 空間佔用的類型,則直接返回即可。於是加上下面這個修改:

diff --git a/src/Sema/comptime_ptr_access.zig b/src/Sema/comptime_ptr_access.zig
index ceddb9457d..2e21c31f2b 100644
--- a/src/Sema/comptime_ptr_access.zig
+++ b/src/Sema/comptime_ptr_access.zig
@@ -65,6 +65,15 @@ pub fn storeComptimePtr(
     const zcu = pt.zcu;
     const ptr_info = ptr.typeOf(zcu).ptrInfo(zcu);
     assert(store_val.typeOf(zcu).toIntern() == ptr_info.child);
+
+    {
+        const store_ty: Type = .fromInterned(ptr_info.child);
+        if (!try store_ty.comptimeOnlySema(pt) and !try store_ty.hasRuntimeBitsIgnoreComptimeSema(pt)) {
+            // zero-bit store; nothing to do
+            return .success;
+        }
+    }
+
     // TODO: host size for vectors is terrible
     const host_bits = switch (ptr_info.flags.vector_index) {
         .none => ptr_info.packed_offset.host_size * 8,

加上這個修改後,重新使用 zig build 命令編譯,然後使用編譯出的 zig 編譯器來測試發生錯誤的例子代碼,具體命令是:

zig-out/bin/zig test crash-reinterp-0-array.zig

發現還有問題,再次查找問題後發現計算類型的 bit 佔用的函數 bitSizeInner 對 array 類型的計算有錯誤,沒有返回 0。具體原因是錯誤的認爲 zero-bit 佔用的類型的 alignment 是 0,實際上正確的是 1。所以修改了 array 類型的計算 bit 佔用的邏輯,具體如下:

diff --git a/src/Type.zig b/src/Type.zig
index 3208cf522d..321730067d 100644
--- a/src/Type.zig
+++ b/src/Type.zig
@@ -1740,10 +1740,7 @@ pub fn bitSizeInner(
             const len = array_type.lenIncludingSentinel();
             if (len == 0) return 0;
             const elem_ty = Type.fromInterned(array_type.child);
-            const elem_size = @max(
-                (try elem_ty.abiAlignmentInner(strat_lazy, zcu, tid)).scalar.toByteUnits() orelse 0,
-                (try elem_ty.abiSizeInner(strat_lazy, zcu, tid)).scalar,
-            );
+            const elem_size = (try elem_ty.abiSizeInner(strat_lazy, zcu, tid)).scalar;
             if (elem_size == 0) return 0;
             const elem_bit_size = try elem_ty.bitSizeInner(strat, zcu, tid);
             return (len - 1) * 8 * elem_size + elem_bit_size;

加上這個修改後,問題完美解決。

在提交代碼到 Zig 到主線前,還需要增加這個 Bug 相關的測試用例。主要是增加 behavior 下的測試用例,在文件‎test/behavior/comptime_memory.zig 中增加這個 Bug 相關的測試用例。具體代碼如下:

test "comptime store of reinterpreted zero-bit type" {
     const S = struct {
         fn doTheTest(comptime T: type) void {
             comptime var buf: T = undefined;
             const ptr: *void = @ptrCast(&buf);
             ptr.* = {};
         }
     };
     S.doTheTest(void);
     S.doTheTest(u0);
     S.doTheTest([0]u8);
     S.doTheTest([1]u0);
     S.doTheTest([5]u0);
     S.doTheTest([5]void);
     S.doTheTest(packed struct(u0) {});
 }

爲了驗證這個修改是否會引發其他問題,需要在本地做 behavior 下的所有測試。具體命令如下:

zig-out/bin/zig build test-behavior \
    -Dlog \
    -Denable-llvm \
    -Dconfig_h=build/config.h \
    -Dno-langref \
    --zig-lib-dir lib

這些測試都做完後,就可以往主線上提交代碼了,使用 git push fix-reinterp-0-sized-array 將代碼 push 到你 fork 的 Zig 代碼倉庫上,然後創建一個 PR 給 Zig 代碼的主倉庫。這樣就完成了代碼提交的工作。需要注意你的代碼是否和主線相差太大,如果相差太大無法合併,則需要重新拉取主線代碼,將你的代碼 rebase 到最新的主線代碼上。重新編譯 Zig 編譯器,並做上面的測試,確認沒有問題後,重新提交代碼。PR 提交後,會有人 review 你的代碼,並提出問題,根據 review 後的建議修改代碼,重新編譯、運行、測試,確認沒有問題後提交代碼和 PR。這個過程可能會有多次,要有耐心,因爲 Zig 代碼的 review 是很嚴格的。

需要注意的是,上述流程中使用的都是 Debug 模式的 Zig 編譯器,因爲這樣可以更方便查找 crash 時出現的問題。在解決問題後,可以編譯 ReleaseFast 模式的 Zig 編譯器。

以上就是 Zig 編譯器的完整開發和調試流程,以及如何提交一個 PR 的過程。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://zhuanlan.zhihu.com/p/1902709976801186537?utm_source=wechat_session&utm_medium=social&utm_oi=31930170998784&utm_content=group3_article&utm_campaign=shareopn&wechatShare=1&s_r=0