Home

Debugger 如何運作的

Debug 就我知道一般有三種常見的機制

  1. 使用 int 3 (x86 or x64) 觸發軟體中斷
  2. 攔截 syscall (這上面有人提到)
  3. 使用 TF flag

另外以下有幾點注意:

  1. 以下環境以 Linux x86 or x64 為主 不過 Windows 的機制也是類似的
  2. int 3 我習慣稱 CC 原因為 X86 下的機器碼的 16 進制正好為 CC

使用 int 3:

這個其實才是我們常說的中斷點,debugger 正是靠在程式中插入 CC 來達成中斷效果。 “不是都轉成機器碼了嗎”,是啊,這有什麼問題呢?一般會有的疑問應該是以下兩點:

  1. 要怎麼修改執行中的程式
  2. 要怎麼知道在哪個位置插入 CC 指令 上面有人有提到 ptrace 可以去看一下 man page 中對 ptrace 的說明

這邊大概列一下 ptrace 有的功能:

  1. 取得/修改暫存器
  2. 取得/修改程式資料
  3. 中斷在下一個 syscall (之後說明)
  4. 單步執行 (之後說明)

其中我們這邊關心的是第 2 個:”取得/修改程式資料”,這個功能讓你可以在程式執行的過程中修改程式在記憶體中的資料。接著這邊有個很重要的觀念

編譯好的程式(機器碼)本身也是一種資料,用來描述 CPU 該如何執行的資料。

機器碼本身會被載入到記憶體中,被 CPU 讀取,並根據它的值 CPU 會做出對應的動作,比如執行加法、跳過下一個指令之類等等。 那麼問題就簡單了,我們只要知道要中斷的指令在哪個位置接著:

  1. 把原本的指令備份後換成 CC
  2. 當程式執行到 CC 時系統就會送出 Signal Trap 通知 debugger
  3. 把修改的指令復原

問題就解決啦,才怪,我們碰到了下一個問題 要怎麼知道該中斷在哪邊

這邊又分成兩種情況:

  1. 你在 debugger 看到的是反組譯的 code 也就是你看到的是組語
  2. 你看到的是原始碼

兩種都可以讓你下中斷點 1 比較簡單,一個組語就對應到一個指令,把那個指令換成 CC 就行了。 2 的情況我們就又要扯到另一個東西了 debug symbol 它紀錄的原始碼與編譯後的結果之間的對應關係。目前主流格式有 stabs, COFF, XCOFF, DWARF 這些只是名字,這邊不會特別做說明。 如果你使用 gcc 編譯時加上了 -g 選項 gcc 就會幫你加上這個東西。 比較有趣的是 gcc 附上的 debug symbol 其實有加上自己用來給 gdb 看的擴充,那麼到這邊,兩個問題都解決了。

攔截 syscall:

關於這個,我正好有找到網路上有人解釋它是如何運作的 https://gist.github.com/RustJason/4e05f6b65448be376fc0 雖說我在這之前也有自己翻一下 code 就是了,上面那個連結裡面解釋的很簡潔,簡單來說: 當你使用 ptrace 要求攔截 syscall 時,它會設定一個 flag 指名要 trace syscall 而當系統發現有這個 flag 時,就會中斷並通知 debugger,這就是上面提到的 ptrace 的第 3 個功能

TF flag:

TF flag 是我們 x86 系列的 CPU 有的一個 flag 它的功能是設定程式為單步執行模式 每執行一個指令 CPU 便會中斷一次 CPU 的中斷會通知 Kernel Kernel 又通知 debugger 這正是上面 ptrace 的第 4 個功能 順帶一提 這個 flag 甚至還有它專屬的 wiki 頁面 https://en.wikipedia.org/wiki/Trap_flag

接下來來個好玩的實作吧 這邊示範如何用 ptrace 修改執行中的程式 原始碼:https://gist.github.com/DanSnow/c29ac0dcf653b4e8cbacfac4dc4e4a81 程式中一開始先讀取原本的機器碼 修改後再寫回去相同的位置 至於那個記憶體的位置與機器碼該如何修改 我是用 objdump 得到的 附圖 1 底下附上的是原本的執行結果與被修改後的執行結果 附圖 2

此外上面提到的軟體中斷是指 interrupt 那是一種來自硬體,同時也可由軟體觸發的一種通知機制,它會打斷 CPU 目前的工作,並跳到 OS 設定好的位置,讓 OS 處理,其中包含比如鍵盤按鍵就會觸發中斷

More...

執行檔的結構

我們都知道 c c++ 這類的語言要經過編譯產出一個執行檔才能執行
這個執行檔的內容中,一般都會包含三個段: text rodata data

  • text: 儲存編譯完 已經是機器碼的程式
  • rodata: 唯讀的資料 比如說用 const 宣告的全域變數
  • data: 可寫的資料 比如全域變數 還有 static 的變數

需要注意的一點是區域變數還有用 malloc 之類的分配的變數都不存在於 data 中 它們都是在執行時才分配的
以上三個段的資料就是編譯器在編譯完後能決定大小的部份
另外通常還會有一個 bss 段 這個段是存全域但是沒初始化的變數
這個段只有在執行檔的標頭中標記大小而已 沒有實際存在於執行檔中
因為沒有初始值 自然不需要佔用硬碟的儲存空間

上面說到執行檔分成三個段 自然的執行檔中還要儲存這三個段的資訊
比如位置 大小 屬性(可寫?唯讀?可執行?)
這些資訊在 windows 的執行檔中是以 PE 格式儲存的
Linux 的執行檔則是 ELF 格式 事實上兩個格式的概念是一樣的
PE 格式你可以用 ExeInfo 這個軟體去讀 ELF 則有 readelf
這邊有個 readelf 的範例:

指令是 readelf -S <執行檔> 我刪掉了不重要(懶的說明)的部份 你在這邊可以看到剛剛說的 3 個段 大小是它們實際佔用的大小 也是它們在硬碟中佔的大小 單位是 bytes 位址是它們被載入記憶體後應該要擺放的位置 可以看到 .text 要被放到 0x400430 順帶一提 通常 windows 下這個位址會是 0x400000 後面的編移量則是在檔案中的位置 如果你找一個 16 進位的編輯器移動到 rodata 段的位置(0x5c0) 你應該會在這附近看到你使用的字串常數

part-of-exe-show-in-hex

(“Hello world\n” 這樣的叫字串常數)
旗標則是這個區段的屬性 A 代表要分配記憶體 X 是可執行 W 是可寫

接下來來看在記憶體中這些段長什麼樣子 https://pastebin.com/5kNZpBNJ
在 linux 下這份資訊可以從 /proc/[pid]/maps 找到
其中可以注意到 記憶體位置對齊了 0x1000 換算下來是 4KB
這個其實是分頁的大小 也就是作業系統分配記憶體的最小單位
perms 代表的是權限 分別有 r 代表可讀 w 可寫 x 可執行
分別再去對照剛剛用 readelf 讀出來的資料會發現
作業系統把 .text 跟 .rodata 合併在同一頁了
.data 段被 map 到 0x601000
然後你還可以在這裡面看到 heap 跟 stack 被分配在哪邊
自己拿計算機按一下 算一下它們的大小多大吧

接下來我們用另一種方式看一下程式在記憶體中長成什麼樣子
先附上 maps 的內容 https://pastebin.com/3Hzq6KwJ
這是用 gdb 執行這個範例程式的結果 [附圖 2]
首先下 b _start 再下 r 把程式跑起來
你會看到目前中斷在 _start 的位置 這是程式的進入點
到這可能會有個問題 程式的進入點不是 main 嗎
是啊 你的程式的進入點確實是 main 可是編譯器會幫你加上一段初始化的 code
這樣才能準備 argc argv 還有一些其它的準備工作
另外你的 main 也才有地方可以 return
(是的 在 _start 中是沒有 return 的 它會直接呼叫 system call 結束自己)
(如果在 _start 中使用了 ret 指令 也就是 return 的組語版 可是會 crash 的)
接著我們執行到 main [附圖 3]
下 b main 再下 c
這畫面中比較重要的一個資訊是 rsp 也就是 stack 的頂端位置
用 rsp 的值減掉剛剛 maps 的內容中 stack 的結尾位置
大約是 11KB 也就是你要溢位這麼多的資料才會碰到下一個分頁
這樣才會觸發作業系統的保護機制把你的程式 kill 掉
另外程式在執行時會透過減少 rsp 的值來分配區域變數
比如把 rsp 減 4 那我們就分配好了一個 int 大小的記憶體了
然後我們用 gdb 印出在 0x4005c0(.rodata) 附近的資料 [附圖 4]
你會看到我們的字串 “Hello world” 這邊可以跟我們一開始的 readelf 對照

More...

用 gcc 做預處理

C 語言裡的 macro 功能真的不錯用
很多常數都是用 #define 做定義的
但如果今天使用的不是 C 語言卻需要這些常數時
除了一個一個複製貼上外,有沒有更好的辦法呢

More...

webpack 的 CommonChunkPlugin 的使用方式

webpack 不是只能將檔案打包成單一個檔案
有時我們也會打包成多個檔案,有可能是因為網頁本來就有多個頁面
或是為了加快載入速度,而不要讓網頁一開始就載入整包 js
但是產生的檔案之中可能存在不少重覆打包的 js
這時就要靠 CommonChunkPlugin 將它們獨立打包成一個檔案
一來可以減少大小,二來可以讓檔案被 cache 而加快載入速度

More...

React 與 Vue.js 的比較

現在正流行的兩個前端框架,ReactVue.js
事實上它們的比較網路上也可以找到不少
這篇文章會從各個角度比較這兩個框架
但請注意,我不會告訴你哪個比較好,或幫你決定你該使用哪個

More...