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

Day02 - 綠鬣蜥初級兵新手訓練營:Zig 基礎語法(上)
CX330走在時代前沿的前言
歡迎回來小蜥蜴們,我是 CX330!昨天已經讓大家把環境給安裝好了,今天就會正式進入我們的暗影綠鬣蜥訓練!語法的部分我總共打算分為 3 天,也就是 3 篇文章來講解。今天的話會說明一下我對於 Zig 語言的認識以及他的一些特性、構建系統、變數宣告、基礎變數型別等。前面的三天都是語法居多,也滿多內容和範例都是來自於 Zig 語言聖經[1],滿推薦可以從這裡入門的。
如果有學過 C 語言的話,應該會覺得 Zig 寫起來手感滿像的,上手起來應該會比較快(相較於隔壁的 Rust XD)。
疊甲
中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」
本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!
Zig 版本
本系列文章中使用的 Zig 版本號為 0.14.1。
我眼中的 Zig
比起很多其他的語言宣稱要成為一個 Better C 來說,於我而言 Zig 更為謙遜且現代,無論是從語法上或是從官方實作的許多功能來看皆是如此。在我眼裡 Zig 從未想要「取代」C 這個古老、偉大且幾乎不可撼動的語言,相反,它更傾向於與 C 語言合作。舉幾個例子,像是 zig cc
完全兼容了 gcc
和 clang
的用法,可以直接作為一個 C/C++ 的編譯器,甚至讓跨平台編譯(Cross compilation)變成內建支援的功能。除此之外,Zig 和 C 也可以直接呼叫彼此的函數庫、標頭檔等(此部分後續也會在實作中遇到),甚至可以用 zig translate-c
來翻譯用 C 寫的原始程式碼。
由上述例子我們可以很好的發現,Zig 跟 C 更像是一個「互助合作」的關係,而非競爭。透過 Zig 編寫的程式也可以更好的避免一些原先在 C 語言中容易發生的錯誤與漏洞,使得程式更加的安全且 Robust(不想講魯棒…)。那如果我們以惡意程式開發的角度思考,為什麼我們要選擇 Zig 這門語言來寫 Malware 呢?我的答案包括但不限於以下幾點。
- 惡意程式中有許多 Code snippet 是 C 語言撰寫,可以互相調用
- 足夠底層,需要寫 Inline assembly 的時候很方便
- 語言現代且編譯器優化導致逆向工程師分析較為困難,同時現有的分析工具也較少
- 構建系統很方便
- 原生高度支援跨平台編譯
寫起來比寫 Rust 快樂 XD
聽起來很不錯!說了這麼多,我們快點來正式看看 Zig 語言吧!我們先從構建系統開始說起,來說明一個 Zig 專案的目錄結構以及如何編譯等等,開始嘍!
構建系統
要在這邊講解完 Zig 的構建系統是不可能的,畢竟我只安排了三天的時間讓大家可以快速上手 Zig 這門語言,重點在於後面惡意程式技術的實作。所以我們在這個章節只會簡單說明我們需要知道的一些內容,如果想要知道更詳細的其他細節,歡迎直接閱讀官方文檔。
畢竟是個現代語言,在 Zig 中,我們也可以用類似 Rust 的 cargo init
和 Go 的 go mod init
來初始化一個專案。具體做法如下:
mkdir my_project && cd my_project # create a prject directory and cd into it
zig init # this will init the project "my_project"
完成後,我們便可以獲得一個 my_project
的 Zig 專案了。在裡面我們會看到很多檔案,他的專案結構長得像下面這樣。
.
├── build.zig
├── build.zig.zon
└── src
├── main.zig
└── root.zig
我們由上至下來看一下這些檔案分別是在幹嘛的。
build.zig
- 整份專案的構建腳本
- 設定編譯目標
- 設定編譯選項
- 新增外部模組與函式庫
build.zig.zon
- 這是 Zig 的依賴項及版本描述檔,有點類似於
package.json
- 這是 Zig 的依賴項及版本描述檔,有點類似於
main.zig
- 應該猜得到,就是主要的入口點
- 裡面有個 main 函數
root.zig
- 這是一個範例的函式庫,可以用來寫全域共用的模組、常數、函式等等
既然有 zig init
可以初始化專案,那肯定也有 zig build
去構建專案對吧,當我們今天不只一個程式碼檔案的時候,build.zig
的功能便得以彰顯。而如同上面所說,整份專案的構建腳本為 build.zig
,所以當我們使用 zig build
命令去構建專案時,他就會讀取這份腳本並執行。我們先來看一下原本透過 zig init
創建的 build.zig
長怎樣吧。
其中,所有的內容都會被放在這個名為 build
的 public 的函數中,等等執行 zig build
時就會呼叫這個函數。
pub fn build(b: *std.Build) void {
...
}
在裡面,會先宣告兩個變數用來設定一些編譯選項:編譯目標以及編譯器優化設定。
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
這邊顧名思義,target
指的是要編譯為哪種主機架構的檔案;而 optimize
則是可以設置不同的編譯優化,有四種預設提供的建構模式(Build mode),分別為:
- Debug
- ReleaseFast
- ReleaseSafe
- ReleaseSmall
接著會建立兩個模組,用於設定要編譯的單位。
const lib_mod = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe_mod.addImport("foo_lib", lib_mod);
這兩個模組將會是編譯器要編譯的檔案,最後一句的 exe_mod.addImport()
會把 lib_mod
用 foo_lib
的名稱匯入到 exe_mod
中,讓 exe_mod
可以使用 @import("foo_lib")
來引用 lib_mod
的內容。
接著會建立靜態的函式庫。
const lib = b.addLibrary(.{
.linkage = .static,
.name = "foo",
.root_module = lib_mod,
});
b.installArtifact(lib);
這邊會把先前建立好的模組建立成靜態函式庫,會先定義要把它加入為一個 library 然後使用 installArtifact
來安裝其到專案的安裝目錄。
然後對執行檔模組建立成執行檔。
const exe = b.addExecutable(.{
.name = "foo",
.root_module = exe_mod,
});
b.installArtifact(exe);
和上面差不多的概念,只不會不是函式庫而是安裝為可執行文件。
後續的步驟,就是建立當使用 zig build run
的時候要執行的步驟,還有一些測試等等,這邊就不細講了。如果有興趣歡迎去看一下裡面的註解,一開始用 zig init
出來的裡面都會有完整的註解可以看。
變數和常數宣告
變數
在 Zig 語言裏,我們會用 var
關鍵字來宣告一個變數,他的格式為:
var variable_name: Type = init_value;
舉例而言,要宣告一個初始化值為 1337 的 16-bit unsigned integer,我們可以使用:
var num: u16 = 1337;
在 Zig 語言中,我們會盡量遵循「非必要不使用變數」的原則,盡可能地使用常數去將可變性局限在需要的最小範圍內。
同時,Zig 語言要求所有非最上層所定義的常數和變數都必須被使用,如果未被使用,則編譯器會噴錯。不過可以像 Python 一樣透過賦值給 _
來將其值拋棄。
常數
而至於常數的話也十分簡單,是使用 const
關鍵字來宣告的。和所有語言一樣,常數宣告之後不可再變更其值,只能在初次宣告的時候對其賦值。舉例而言,要宣告一個初始化值為 1337 的 16-bit unsinged integer 常數,我們可以使用:
const num: u16 = 1337;
標示符命名
先來講講變數和常數名名稱的命名吧!在 Zig 語言中,一個變數或常數的標示符必須透過字母或底線開頭,後面可以有字母、數字、或底線,同時不得語關鍵字重複。並且,也禁止存在變數遮蔽(Variable shadowing),內部作用域的變數名稱不得與外部的相同,我們看一下以下案例。
const std = @import("std");
const a = 5;
pub fn main() void {
const a = 6;
}
我們可以發現在最上層和 main
函數中,都有一個稱為 a
的變數,那這時候 Zig 就會在編譯期報錯,說發現了有變數遮蔽的問題,如下。
test.zig:6:11: error: local constant shadows declaration of 'a'
const a = 6;
^
test.zig:3:1: note: declared here
const a = 5;
^~~~~~~~~~~
如果真的硬要使用這些不合規範的變數名稱,像是要連結外部的函式庫的時候,可以使用 @""
的語法,例如:
pub extern "c" fn @"error"() void;
未初始化的變數
如果說,想像 C 語言一樣先宣告一個變數,晚一點再來賦值,我們可以使用 undefined
關鍵字來讓變數保持在未初始化的狀態。
var num: u16 = undefined;
使用 undefined
初始化的變數不會有任何的初始化操作(不會初始化為 0),常常用於初始值不重要的情境底下,例如用作 buffer 的變數通常會被傳遞給另一個函數進行寫入,會覆蓋原有的內容,故初始值在此情境下就不重要。
解構賦值
這就很 Javascript,如果寫過 JS 的應該會超級熟悉。解構賦值(Destructing assignment)就是允許對可索引的複雜結構進行解構,並賦值給指定的變數(這樣有解釋到嗎 XD)。我們看個例子就明白了。
var a: u32 = undefined;
var b: u32 = undefined;
var c: u32 = undefined;
const tuple = .{ 1, 2, 3 }; // here!
x, y, z = tuple;
// Result:
// x = 1
// y = 2
// z = 3
在這個例子中,第 5 行就是解構賦值發生的地方,會直接把元組中的資料一一對應的賦值給三個變數。
註解
Zig 有三種註解模式:
- 普通註解
- 文檔註解
- 頂層文檔註解
普通註解
普通註解跟 C 語言一樣是用 //
的符號表示,挺常見的。不過值得一提的是,Zig 中並沒有像 C 的 /* */
或是 Python 的 """ """
這種多行註解符號的存在,所以當註解很多行的時候會在每行的開頭使用 //
。
文檔註解
使用 ///
就可以寫文檔註解,這東西就是比如你寫程式,游標 hover 某個變數名稱的時候會提示這個函數的功能在做什麼,這就是文檔註解。他會寫在函數、類型、變數等等標示符的上方,以說明該標示符的作用。
頂層文檔註解
頂層文檔註解的話用的是 //!
的符號,它就是整個檔案的註解,通常用於記錄檔案的作用,必須放在作用域的最上層,否則編譯器會噴錯。
基礎變數
數值類型
整數
在 Zig 中宣告一個整數類型非常的自由,可以用 i
表示有符號整數;用 u
來表示無符號整數,並且這個 i
跟 u
後面可以接任意的數字,來表示位元長度。舉例來說,我想宣告一個 1337 位元的無符號整數,就可以使用 u1337
來表示,酷吧!對了,這個寬度的最大上限是 65535。
然後還有 isize
跟 usize
兩種型別,這兩種型別的大小取決於目標的 CPU 架構,在 32 位元的系統中就是 32,64 位元亦然。
進位制
如同許多其他的程式語言,Zig 也可以簡單使用不同的方式表達不同的整數進位制。
寫法 | 範例 |
---|---|
10 進位 | 1337 |
16 進位 | 0x539 |
8 進位 | 0o2471 |
2 進位 | 0b10100111001 |
浮點數
Zig 的浮點數有 f16
、f32
、f64
、f80
、f128
和 c_longdouble
(對應 C ABI 的 long double
)。
另外,Zig 沒有提供像其他語言那樣的 NaN
、無限大、負無限大等這種語法,若需要使用它們,需要透過標準函式庫獲取:
const std = @import("std");
const infinite = std.math.inf(f32);
const neg_infinite = -std.math.inf(f64);
const nan = std.math.nan(f128);
運算子
哎呀什麼加減乘除大於等於小於的我就不講了,和其他程式語言都一樣,我主要講一些和其他語言不同的部分。
邏輯的與、或、非使用的是 and
、or
、!
來表示,而位元操作的與、或、非則是使用 &
、|
、~
。而按位的 XOR 也是和其他語言差不多,是用 ^
符號。
除了這些之外,Zig 還有一些很酷的運算,例如飽和運算。飽和運算指的是,運算過後的值不會超過該類型的極值。他的語法就是在一般的操作後面加上 |
符號,例如飽和加法就是 +|
而飽和乘法就是 *|
。舉例來說,一個 u8
類型的 255 透過飽和加法加 1 之後仍然會是 255。
那飽和運算的相反,就是環繞運算了。在 C 語言中 Integer overflow 的預設行為,就是會從另一端環繞回來。舉例來說,一個 8-bit unsigned integer 的 255 再加上 1 就會變回 0,這在 C 語言中是預設的行為。但為了保障數值的安全性,這種行為在 Zig 中卻需要特別去處理。如果希望數字可以 overflow 回來,要用「環繞運算子」,語法就是在普通運算子的最後面加上 %
符號,比如 +%
和 *%
等等。
字串與布林值
字符串字面量(String Literal)
字符串字面量就是 String Literal,雖說是中國的翻譯,但我在台灣比較少看到這詞的翻譯,故用之,不過我以下會用英文來取代「字符串字面量」這個詞。
寫過 C 的大家應該都知道,String literal 就是字元的陣列,這個概念在 Zig 裡面也是一樣的。和 C 語言一樣,字串是一個字元指針,指向一個字元陣列的開頭,並由 Null terminator 結束(0
)。不過既然因為 String literal 是字元的陣列,因此字符串可以隱式轉型為 u8
的切片,或是 0 結尾的指針。我們可以看一下以下的程式碼,我們先把 bytes
的型別打印出來,發現是 *const [12:0]u8
的型別。
const std = @import("std");
const print = std.debug.print;
pub fn main() void {
// Storing UTF-8 encoding sequence
const bytes = "你好, Zig!";
print("{}\n", .{@TypeOf(bytes)}); // *const [12:0]u8
print("{}\n", .{bytes.len}); // 12
print("{x}\n", .{bytes[0]}); // 'e4'
print("{x}\n", .{bytes[1]}); // 'bd'
print("{x}\n", .{bytes[2]}); // 'a0'
// Ended by NUL
print("{d}\n", .{bytes[12]}); // 0
}
而上面的 e4 bd a0
就是 你
的 UTF-8 的三個位元組,並且也可以看到最後一位確實是 0
,也就是 Null terminator。
多行字符串
在 Zig 語言中的多行字符串是用 \\
在每一行的開頭,不會有任何轉義,也不包含最後一行的換行符。
const multi_line_str =
\\Hi,
\\My name is CX330.
;
C 語言的 char
就像我說過的,Zig 和 C 有著很和諧的合作關係,因此它也提供了一個類型叫 c_char
來提供 C 語言的字元型別。即便 c_char
和 u8
都是 8 個位元,但是他們兩個不完全等價,這是因為 c_char
是否有號是取決於目標機器的。
因此,除非是與 C 語言做交互,平常時間還是更推薦使用 u8
型別來操作字串和字元等相關型別。
布林值
這就很簡單,就是 true
和 false
而已。
函數
初見 Function
寫過 C 的在看 Zig 語言的函數應該很容易看懂,我舉個例子。
pub fn add(a: u8, b: u8) u8 {
return a + b;
}
這個函數就是一個兩數相加的函數,具體說明如下:
pub
是表示這個函數可以被公共存取,代表其他文件import
之後可以直接使用fn
是宣告函數的關鍵字add
是標示符,就是函數名稱a: u8, b: u8
是參數的名稱和類型,有a
和b
兩個參數,並都是u8
型別u8
是函數的返回值類型
像 C 語言一樣,若沒有返回值,應該使用 void
關鍵字。
參數傳遞
在 Zig 裡面的參數傳遞如下:
- 原始類型(Primitive type)
- Pass by value
- 整數
- 布林值
- 複合類型(Composite type)
- 編譯器會根據具體情況決定是 Pass by value/reference
- 結構體
- 聯合體
- 陣列
可能很難捉摸,但我們只需要記住,函數的參數是不可變的!所以當需要變更傳遞的參數的時候,請使用指針。
鐵人賽期 PoPoo,你今天轉 Po 了嗎?
好啦,恭喜大家第二天結束囉!今天帶大家看了 Zig 的一些特性還有他的構建系統、變數宣告、基礎型別等等。明天的一開始會帶大家寫一個 Hello world 的程式,同時介紹一些複合類型和流程控制等。那就請大家期待新手訓練營的第二天啦!
如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!