Day03 - 綠鬣蜥初級兵新手訓練營:Zig 基礎語法(中)

走在時代前沿的前言

歡迎回來小蜥蜴們,我 CX330!今天是我們新手訓練營的第二天!昨天和大家介紹了 Zig 的一些基礎語法,今天要來帶大家寫一下 Hello world 並且了解更多複合的型別(昨天有說今天要講流程控制,但礙於偏於我會挪到明天再介紹)。

如果還沒讀過之前的文章,可以跳過去先看看!好的,那我們就開始嘍!

哦對了,為什麼 iThome 的這個程式碼 Highlight 沒有支援 Zig,可惡!然後查了一下用的是 highlight.js,然後又翻了一下發現 highlight.js 已經在 2024-01-02 支援了 Zig,但是是由第三方的 Package 支援的,iThome 應該是沒有使用該套件,希望他們早日支援 T^T。

疊甲

中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」

本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!

Zig 版本

本系列文章中使用的 Zig 版本號為 0.14.1。

Hello World

要用 Zig 寫一個 Hello world 也是很簡單的,直接寫一下大家就能看懂。

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, world\n", .{});
}

我這邊逐行解釋一下。第一行的 std 是把標準函式庫 Import 進來的變數名稱,接著會定義一個函數叫做 main,由於是主函式,所以需要是 pub 的才能被執行,這個函式沒有參數也沒有回傳值。並且裡面的 std.debug.print 就是 Zig 的 print 函式了,他後面的 .{} 就是會提供值給前面的格式化字符串。比如說

std.debug.print("a = {d}", .{a});

就會把 a 變數作為整數({d})給打印出來。但這邊打印整坨 "Hello, world" 字串,所以裡面留空即可。

接著要編譯他為一個執行檔,就可以使用以下的語法。

zig build-exe hello.zig  # hello.zig 就是這隻程式的程式碼

編譯完成後,就可以執行啦!下面我們來看一下結果。

hello world

恭喜大家,已經成功寫出第一隻 Zig 程式了!接著讓我們繼續看看更酷的東西吧!

複合類型(Composite type)

陣列 Array

陣列的類型為 [N]T。和 C 的陣列非常像,都擁有以下特性。

  • 長度固定
  • 元素類型必須相同
  • 依次線性排列

宣告陣列

直接看程式碼。

const msg1 = [5]u8{ 'h', 'e', 'l', 'l', 'o' };
const msg2 = [_]u8{ 'h', 'e', 'l', 'l', 'o' };

如上面的程式碼,我們可以明確指定陣列的長度,也可以用 _ 符號讓 Zig 自動推斷陣列的長度。由於陣列是連續在記憶體中的,故我們可以使用所以來存取每個元素,例如:

std.debug.print("{c}\n", .{msg1[0]});

就會打印出 h。至於 Out of bound 的問題,Zig 也在編譯期和執行期都提供了完整的保護與錯誤訊息。

多維陣列

一樣是跟 C 很像,我們還是直接看程式碼比較快。

const print = @import("std").debug.print;

pub fn main() void {
    const matrix_4x4 = [4][4]f32{
        [_]f32{ 1.0, 0.0, 0.0, 0.0 },
        [_]f32{ 0.0, 1.0, 0.0, 1.0 },
        [_]f32{ 0.0, 0.0, 1.0, 0.0 },
        [_]f32{ 0.0, 0.0, 0.0, 1.0 },
    };

    for (matrix_4x4, 0..) |arr_val, arr_index| {
        for (arr_val, 0..) |val, index| {
            print("Element {}-{} is {}\n", .{ arr_index, index, val });
        }
    }
}

其實就只是陣列裡面還有陣列,沒什麼太複雜的東西。至於 for 迴圈的部分如果有地方不太理解,我們後面會講。

陣列運算

Zig 的陣列可以像是 Python 的 List 一樣相加和相乘,如果不知道 Python 的 List 的相加相乘,大概是像這樣。

In [1]: a = [1, 2, 3]

In [2]: a * 3
Out[2]: [1, 2, 3, 1, 2, 3, 1, 2, 3]

In [3]: b = [4, 5, 6]

In [4]: a + b
Out[4]: [1, 2, 3, 4, 5, 6]

那在 Zig 裡面的做法是使用 ** 來做陣列的複製(乘法),用 ++ 來做陣列的串聯(加法)。

指針 Pointers

指針是一個用來儲存地址的變數,透過指針,程式設計師就可以間接訪問和操作其指向的記憶體區域。接下來想請大家跟我一起默念三下:

指針就只是個地址

指針就只是個地址

指針就只是個地址

和 C 語言相同,要獲取某個變數的地址是透過 & 取址符號獲得,像是 &num 就是獲得 num 的記憶體地址。

和 C 語言不一樣的地方在於,Zig 的指針有兩種類型:單項指針和多項指針。它主要區分的是指針指向的元素為單個還是多個,進而更安全、高效的使用指針。這個圖解釋了它們的不同。

Zig pointers - Zig 語言聖經

單項指針

他的英文是 Single-item pointer,為指向單個元素的指針。單項指針的類型為 *T,當中的 T 是所指向的類型。Zig 的單項指針支持以下的操作語法:

  • 解引用(Dereference)
    • ptr.*
  • 切片語法
    • ptr[0..1]

多項指針

多項指針的英文是 Many-item pointer,是指向很多個元素的指針。他的類型為 [*]T,並且其中的 T 的大小必須是明確的,也就是不可為 opaque 類型。聽不懂沒事,放心待會會提到。多項指針支持以下的操作:

  • 索引語法
    • ptr[i]
  • 切片語法
    • ptr[start..end] 或是 ptr[start..]
  • 指針運算
    • ptr + int 或是 ptr - int

函數指針

函數指針,就是一個指向函數的指針。他其實不是什麼神奇玩意,他就是個單項指針,只不過它指向的是一個函數。我們直接來看一段程式碼。

const std = @import("std");
const Op = *const fn (i32, i32) i32;

fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub fn main() void {
    const op: Op = add;
    std.debug.print("{}\n", .{op(2, 3)});
}

一開始先定義了變數 Op 為一個接受兩個 32-bit 有號整數並回傳一個 32-bit 有號整數的函數的指針的型別。接著在 main 裡面把它指向 add 函數,最後再用 op(2, 3) 執行它。你可能會好奇為什麽不用解引用這個函數指針就可以直接呼叫,是因為 Zig 會自動的對函數指針解引用,這點又是跟 C 語言很相似的地方。

切片 Slices

切片和陣列十分相像,類型為 []T 。但因為在實際寫程式碼的時候更靈活,所以我們更常使用切片。切片常常被稱為胖指針(Fat pointers),因為他們的大小是普通指針的兩倍,其原因在於,它本質上是一個多項指針加上一個長度值。所以我們可以透過 .ptr.len 來操作一個切片的指針和長度值。

我們可以透過陣列、其他切片或是陣列指針來創建新的切片,如下:

var array = [_]i32{ 1, 2, 3, 4 };
const slice: []i32 = array[0..];

這樣就成功地獲取了 array 的切片 slice 了!這邊注意一下,這個 [0..] 也一樣是左閉右開的邊界。

還記得我們說切片是個胖指針,是個多項指針加上長度。所以當我們想要獲取一個切片指針,可以用 .ptr 的語法存取。但如果我們想得到某個元素的指針,就可以用 & 符號獲得,像是 &slice[0]

結構體 Structs

結構體是用來將許多相關的資料類型組織為一個單一的實體的類型。結構體的組成是由以下的東西所構成的:

  1. 關鍵字 struct
  2. 結構體名稱
  3. 方法
  4. 變數(們)

以下舉個例子,定義了一個 Circle 結構體,並且提供了 initarea 兩個方法。

const Circle = struct {
    radius: u8,

    const PI: f16 = 3.14;

    pub fn init(radius: u8) Circle {
        return Circle{ .radius = radius };
    }

    fn area(self: *Circle) f16 {
        return @as(f16, @floatFromInt(self.radius * self.radius)) * PI;
    }
};

這段程式碼定義了一個結構體 Circle,並包含了 radius 變數,在調用 init 方法的時候會需要傳入相對應的 radius。除此之外,他還包含一個常數 PI 和計算面積的方法 area

自動推斷

Zig 在使用結構體的時候支持省略類型的宣告,Zig 會自動進行類型推斷,舉例來說:

const Point = struct { x: i32, y: i32 };

const pt: Point = .{
    .x = 13,
    .y = 67,
};

枚舉 Enums

枚舉這個結構會用於表示一個有限的集合的成員,比如表示狀態或是表示類別等等。要宣告一個枚舉,我們可以使用 enum 關鍵字,舉個例子:

const Status = enum {
	ok,
	not_ok,
};

const s = Status.ok;

除此之外,我們也可以手動制定枚舉的 tag 的類型,舉例來說:

const Value = enum(u2) {
	zero,
	one,
	two
}

而且當我們手動指定 tag 類型的時候,就可以手動操作每個成員的值,例如:

const Value = enum(u32) {
	hundred = 100,
	thousand = 1000,
	million = 1000000,
};

同時,枚舉也支持自動類型推斷,可以直接使用 tag 名稱,例如:

const Color = enum {
	blue,
	red,
	yellow,
};

pub fn main() !void {
	const c1: Color = .blue;
	const c2: Color = Color.blue;
	_ = (c1 == c2);	 // this is true
}

聯合體 Unions

聯合體是一個用來存放多種不同類型的值的結構,它與結構體的差異在於,他同時只能儲存其中某一種類型的值

const MyUnion = union {
	int: i64,
	float: f64,
	boolean: bool,
};

pub fn main() !void {
	var u = MyUnion{ .int = 7 };
	u = MyUnion{ .int = 1337 };
	
	std.debug.print("{}\n", .{u.int});
}

Tagged Unions

聯合體在定義的時候可以使用枚舉進行標記,並且可以通過內建的 @as 函數將聯合體直接視為宣告的枚舉來使用或是做比較。

看不懂的話沒事,簡單來說就是普通的 Unions 可以存放多種值,但是無法知道當前儲存的是哪種類型,而 Tagged unions 可以在一般的 Unions 的基礎上增加了類型追蹤的能力,讓他更好用(安全且易用)。

普通的聯合體在 ReleaseSmallReleaseFast 的構建模式下,會無法檢測出錯誤的讀取行為。如果將一個 u64 儲存在一個聯合體之中,並將其讀取為 f64,在這樣的構建模式中就不會噴錯。

為了讓大家更理解 Tagged Unions,舉個例子好了,或許會比較易懂!

const MyTag = enum {
	ok,
	not_ok,
};

const MyUnion = union(MyTag) {
	ok: u8,
	not_ok: void,
};

const u = MyUnion{ .ok = 42 };

_ = (@as(MyTag, u) == MyTag.ok);	// This is true

注意上面的程式碼的最後一行,透過 @as 我們可以將 u 轉為它的標記的類型,並與 MyTag.ok 做比較。

自動推斷

聯合體類型也支援自動推斷類型。

const Number = union {
	int: i32,
	float: f64,
};

const n: Number = .{ .int = 42 };

opaque

這是用來宣告一個大小和對齊方式都未知但非零的新類型,其內部結構可以包含定義(像結構體、聯合體或是枚舉一樣)。這常常會用來和 C 程式碼做交互,以確保類型安全,尤其是在像跟 Windows 的很多未公開細節的結構體互動的情況下。

下面的程式碼是個範例:

const Window = opaque {
    fn show(self: *Window) void {
        show_window(self);
    }
};

extern fn show_window(*Window) callconv(.C) void;

test "opaque with declarations" {
    var main_window: *Window = undefined;
    main_window.show();
}

鐵人賽期 PoPoo,你今天轉 Po 了嗎?

好啦小蜥蜴們!今天帶大家學習了 Zig 裡面很多的複合類型,這些類型在以後都會多多少少出現在我們的惡意程式碼之中!那明天就會是整個新手訓練營的最後一天了,會帶大家看一下流程控制、可選類型、錯誤處理等等。當然,由於一個語言可以講的東西實在太多了,之後我們就會直接開始寫程式,等遇到了新的語法的時候再在當下補充做解釋!

如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!