# VSCode 開發 Linux Kernel Module 的準備
> contributed by < [`Shiritai`](https://github.com/Shiritai) >
:::success
我將[改進版的腳本](#腳本法)連同說明文件放置於 [make2intelliSense](https://github.com/Shiritai/make2IntelliSense) repository,歡迎檢閱、使用與不吝指教 :)
:::
## Include Path 設定
以 `fibdrv` 這個作業專案為例,如果純粹將 reposiroty clone 好後使用 VSCode 開啟比如 `fibdrv.c`,相信會看到貼心的 IntelliSense 的警告。

這是因為預設的 include path 並不包含我們需要的核心標頭檔。
### 常規做法
由前車之鑑 [1](https://stackoverflow.com/questions/58386640/how-to-develop-linux-kernel-module-with-vscode-without-incorrect-error-detection) 和 [2](https://github.com/microsoft/vscode-cpptools/issues/5588) 得知 IntelliSense 需要調整 [C/C++ Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) 設定檔: `c_cpp_properties.json`。
1. 準備 Linux 設定檔模板
```json
{
"configurations": [
{
"name": "Linux",
"browse": {
"limitSymbolsToIncludedHeaders": true,
"databaseFilename": ""
},
"intelliSenseMode": "linux-gcc-x64",
"compilerPath": "/usr/bin/gcc",
"cStandard": "c99",
"cppStandard": "gnu++17"
}
],
"version": 4
}
```
2. 加入 `includePath` 設定
```json
...
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/usr/include",
"/usr/local/include",
"/usr/src/LINUX_KERNEL_HEADER/include",
"/usr/src/LINUX_KERNEL_HEADER/arch/x86/include",
"/usr/src/LINUX_KERNEL_HEADER/arch/x86/include/generated",
"/usr/lib/gcc/x86_64-linux-gnu/GCC_VERSION/include"
],
"browse": {
...
```
要調整兩點。
* `LINUX_KERNEL_HEADER` 換成自己的 kernel header 路徑名。
* `GCC_VERSION` 換成自己的 GCC 版本。
基本上 `includePath` 中前五項都是開發 Kernel Module 的常客,當然第七項編譯器本身也是。第六項本次實作會用到,不過未來開發其他模組的話可能會引入其他的標頭檔。
尋找標頭檔的方法為至 `include` 資料夾下尋找被 include 的標頭檔,將其所在的資料夾加入 `includePath`。
## defines 設定
又以 `lab0-c` 作業中探討 `list_sort.c` 為例,即使直接將 `linux` 這個 repository clone 下來並修正 include path 問題,還是會發現一些巨集或函式無法被識別,而被 IntelliSense 自動猜測為 `int FUNC_NAME()`。

:::success
likely 和 unlikely 兩巨集為編譯器提供的巨集,為了提示編譯器判斷分支發生的可能性,編譯器便可以根據此提示進行最佳化,[此文提供了詳細的說明](https://meetonfriday.com/posts/cecba4ef/)。
使用時須引入 `<linux/compiler.h>`,同時可以進入[原始碼](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h)查看巨集的細節。發現與 `__KERNEL__` 以及其他數個巨集有關:
> 以下展示部分,前面還有另一個定義。

:::
這些巨集定義都影響著 IntelliSense 的運作,若不告訴它我們編譯時定義的巨集,IntelliSense 就不會正常運作。
### 常規做法
`defines` 是為了解決條件編譯的問題,當中加入的字串會使 IntelliSense 在 indexing 時看見需指定條件編譯後才會編譯到的程式碼。
基本上加入以下巨集定義就能解決大部分條件編譯的影響。
```json
...
"compilerPath": "/usr/bin/gcc",
"defines": [
"__GNUC__",
"__KERNEL__",
"MODULE"
],
"cStandard": "c99",
...
```
由於前面設定正確的 include path,並加入 `__KERNEL__` 等巨集, IntelliSense 復活了 :)
> Include Path 的例子

> Include Path + 巨集設定的例子

## 編譯後目錄雜亂的問題
正常進行編譯後,以 Linux 核心為例,可能會看到目錄看見以下:
:::spoiler 不怎麼美觀的目錄


:::
不僅不美觀,VSCode 更會去遞迴的解析這些不必要解析的檔案,從而降低 VSCode 的速度,增加搜尋程式碼的負擔。
對此,[VSCode 有很多設定](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)可以調整對特定檔案的排除,不過最便捷的應該是以下兩種。
1. 直接套用 `.gitignore` 的設定
```json
// .vscode/settings.json
{
"explorer.excludeGitIgnore": true,
}
```
2. 利用 `files.exclude` 決定檔案的去留
```json
// .vscode/settings.json
"files.exclude": {
"**/*.a": true,
"**/*.cmd": true,
"**/*.d": true,
"**/*.elf": true,
...
"**/Module.symvers": true,
"**/Thumbs.db": true,
"**/client": true,
"**/dkms.conf": true,
"**/modules.order": true,
"**/out": true
}
```
後者若要手動添加會有點麻煩...
## 腳本法
在開發已經打好基礎之模組的情況下,本方法依賴完好的 `Makefile` 和 `.gitignore` 便可自動生成設定檔,因為 `MakeFile` 是告訴我們編譯方法的第一手資料、`.gitignore` 則是告訴我們哪些檔案沒有顯示的必要。
直接手動追蹤 Makefile 固然可行,但若以 Linux 核心原始碼為例,如果打開其 [Makefile](https://github.com/torvalds/linux/blob/master/Makefile),我們會看到

:::info
以 Linux 6.3.0 為例,兩千一百多行的 `Makefile`...別開玩笑了,這不是肉眼可以追蹤的量吧。對小的專案沒問題,但我們的目標是 Linux 核心...
:::
對此,[本網站](https://iotexpert.com/stupid-python-tricks-vscode-c_cpp_properties-json-for-linux-kernel-development/)提供很暴力但可行的解決方法: 利用 `make` 的 [`--just-print`](https://www.gnu.org/software/make/manual/html_node/Instead-of-Execution.html) 功能 (其會印出若真的運行時會執行的命令),以獲得包含 `includePath`,`defines` 等訊息,透過 Python 腳本解析後生成[方才](#常規做法)提及之常規做法的模板。
本方法的特色有二
* 一次擷取所有需要的 include 路徑
* 巨集可以對個別電腦客製化
但由於一次粗暴的解析所有編譯時加入的巨集,故有兩缺點:
* 解析出的巨集量過多,導致 VSCode 解析程式碼變得緩慢
* 不同程式/模組所需的巨集不同,一次定義好所有巨集不保證適用於所有模組
:::info
Note: 有改善的空間,比如針對不同次編譯為 VSCode 客製化編譯時的巨集定義。不過需要更查詢 VSCode 的官方文件....
:::
針對第一個缺點,我提出了改進版的腳本。當中加入了一些例外處理與客製化設定,另根據[Makefile 語法](https://hackmd.io/@sysprog/gnu-linux-dev/https%3A%2F%2Fhackmd.io%2Fs%2FSySTMXPvl)和官方文件練習寫簡單的 `Makefile`。
:::success
具體請參考[此腳本](https://github.com/Shiritai/make2IntelliSense)。
:::
## 腳本開發紀錄
* 加入對排除檔案的調整
* `exclude`: 手動加入想排除之 pattern 的列表
* `bypass`: 不希望排除之 pattern 的列表
預設加入 `.vscode` 避免其被忽略。
```python
bypass = {
'.vscode'
}
```
* `replace`: 將指定偵測到的排除檔案置換成自己想要的 pattern
* 預設為以下,如此便不會因為 Linux kernel 中有 `.*` (匹配以 `.` 為首之任意長度字串) 忽略 `.vscode` 資料夾。
```python
replace = {
".*": ".[!v]*" # exclude files start with . but not .v
}
```
> VSCode 採用 Unix 下經典的 [glob](https://en.wikipedia.org/wiki/Glob_(programming)) 語法匹配檔名 pattern,但不同於 `bash`,VSCode 並不支援 glob 的擴充語法,不如 regex 靈活。不過語法規則少很多,相信看維基就能理解了。
:::info
可能的改進:
1. 之後考慮將 `gen_file_exclude.py` 的 `exclude`, `replace`, `bypass` 三個參數開放在執行 make 時客製化。
2. 追求更加自動化: 自動偵測指定之不想被屏蔽之檔案可能會被哪些 pattern 屏蔽。
:::
* `make clean` 命令: 嘗試刪除目標資料夾下 .vscode 內的設定檔
* 針對 `c_cpp_properties.json`,新增自動偵測以下客製化設定
* 編譯器路徑
* 使用之 C/C++ 語言標準
* 會採用最低標準,比如若有 `gnu11` 和 `gnu99`,則會採用 `gnu99`。
* 若都沒有指定語言標準,則預設標準為 `c99`,這可能會使 IntelliSense 有些許不符合預期。
方法為使用 regex,正好把以前不願面對的它重新學了一遍。
:::success
以下保留學習的參考連結給也想學 regex 的人。
* [練習場](https://regex101.com/)
* [做中學](https://regexone.com/)
:::