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

Day03 - 綠鬣蜥初級兵新手訓練營:Zig 基礎語法(中)
CX330走在時代前沿的前言
歡迎回來小蜥蜴們,我 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 就是這隻程式的程式碼
編譯完成後,就可以執行啦!下面我們來看一下結果。
恭喜大家,已經成功寫出第一隻 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 的指針有兩種類型:單項指針和多項指針。它主要區分的是指針指向的元素為單個還是多個,進而更安全、高效的使用指針。這個圖解釋了它們的不同。
單項指針
他的英文是 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
結構體是用來將許多相關的資料類型組織為一個單一的實體的類型。結構體的組成是由以下的東西所構成的:
- 關鍵字
struct
- 結構體名稱
- 方法
- 變數(們)
以下舉個例子,定義了一個 Circle
結構體,並且提供了 init
和 area
兩個方法。
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 的基礎上增加了類型追蹤的能力,讓他更好用(安全且易用)。
普通的聯合體在 ReleaseSmall
和 ReleaseFast
的構建模式下,會無法檢測出錯誤的讀取行為。如果將一個 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 裡面很多的複合類型,這些類型在以後都會多多少少出現在我們的惡意程式碼之中!那明天就會是整個新手訓練營的最後一天了,會帶大家看一下流程控制、可選類型、錯誤處理等等。當然,由於一個語言可以講的東西實在太多了,之後我們就會直接開始寫程式,等遇到了新的語法的時候再在當下補充做解釋!
如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!