JavaScript Debugger 原理揭祕

代碼寫完會運行一下看下效果,開發的時候咱們更多都是經過 dubugger 來單步或斷點運行。咱們成天在用 debugger,但是你有想過它的實現原理麼。javascript

本文會解答如下問題:java

  • 代碼運行的底層原理是什麼
  • 爲何須要 debugger
  • debugger 實現原理是什麼
  • 如何實現 debugger 客戶端

代碼運行的原理是什麼

代碼的運行方式能夠分爲直接執行和解釋執行兩類。node

不知道平時你有沒有注意,可執行文件直接 ./xxx 就能夠執行,而執行 js 文件須要 node ./xxx,執行 python 文件須要 python ./xxx,這就是編譯執行(直接執行)和解釋執行的區別。python

直接執行

cpu 提供了一套指令集,基於這套指令集就能夠控制整個計算機的運轉,機器語言的代碼就是由這些指令和對應的操做數構成的,這些機器碼能夠直接跑在計算機上,也就是可直接執行。由它們構成的文件叫作可執行文件。linux

不一樣操做系統可執行文件的格式不一樣,在 windows 上是 pe(Portable Executable) 格式,在 linux、unix 系統上是 elf(Executable Linkable Format) 格式,在 mac 上是 mash-o 格式。它們規定了不一樣的內容(.text 是代碼、.data .bass 等是數據)放在文件中的什麼位置。但其中真正可執行的部分仍是由 cpu 提供的機器指令構成的。git

編譯型語言會通過編譯、彙編、連接的階段,編譯是把源代碼轉成彙編語言構成的中間代碼,彙編是把中間代碼變成目標代碼,連接會把目標代碼組合成可執行文件。這個可執行文件是能夠在操做系統上直接執行的。就由於它是由 cpu 的機器指令構成的,能夠直接控制 cpu。因此能夠直接 ./xxx 就能夠執行。程序員

解釋執行

編譯型語言都是生成可執行文件直接在操做系統上來執行的,不須要安裝解釋器,而 js、python 等解釋型語言的代碼須要用解釋器來跑。github

爲何有了解釋器就不須要生成機器碼了,cpu 仍然不認識這些代碼啊?web

那是由於解釋器是須要編譯成機器碼的,cpu 知道怎麼執行解釋器,而解釋器知道怎麼執行更上層的腳本代碼,就這樣,由機器碼解釋執行解釋器,再由解釋器解釋執行上層代碼,這就是腳本語言的原理。 包括 js、python 等都是這樣。chrome

可是解釋器畢竟多了一層,因此有的時候會把它編譯成機器碼來直接執行,這就是 JIT 編譯器。好比 js 引擎通常就是由 parser、解釋器、JIT 編譯器、GC 構成,大部分代碼是由解釋器解釋執行的,而熱點代碼會通過 JIT 編譯器編譯成由機器碼,直接在操做系統上執行以提升性能。

編譯成機器碼直接執行,或者是從源碼解釋執行,代碼就這兩種執行方式。二者各有各的好處,編譯型速度快,解釋型跨平臺。這就是代碼運行的原理。

王垠說過,計算機的本質就是解釋器。就是說 cpu 用電路解釋機器碼,解釋器用機器碼解釋更上層的腳本代碼,因此計算機的本質是解釋器。

爲何須要 debugger

咱們知道,圖靈完備的語言能夠解釋任何可計算問題,因此不論是編譯型仍是解釋型都可以描述全部可計算的業務邏輯。

咱們利用不一樣的語言描述業務邏輯,而後運行它看效果,當代碼的邏輯比較複雜的時候,不免會出錯,咱們但願可以一步步運行或是運行到某個點停下來,而後看一下當時的環境中的變量,執行某個腳本。完成這個功能的就是 debugger。

也許還有不少初級程序員只會用 console.log 打日誌,可是日誌不能徹底展示當時的環境,最好的方式仍是 debugger。

狼叔說過,是否會用 debugger 是 nodejs 水平的一個明顯的區分

debugger 的原理

咱們知道了 debugger 是調試程序必不可少的,那麼它是怎麼實現的呢?

可執行文件的 debugger

其實 cpu、操做系統在設計的時候就支持了 debugger 的能力(可見 debugger 的重要性),cpu 裏面有 4 個寄存器能夠作硬中斷,操做系統提供了系統調用來作軟中斷。這是編譯型語言的 debugger 實現的基礎。

中斷

cpu 只會不斷的執行下一條指令,但程序運行過程當中不免要處理一些外部的消息,好比 io、網絡、異常等等,因此設計了中斷的機制,cpu 每執行完一條指令,就會去看下中斷標記,是否須要中斷了。就像 event loop 每次 loop 完都要檢查下是否須要渲染同樣。

INT 指令

cpu 支持 INT 指令來觸發中斷,中斷有編號,不一樣的編號有不一樣的處理程序,記錄編號和中斷處理程序的表叫作中斷向量表。其中 INT 3 (3 號中斷)能夠觸發 debugger,這是一種約定。

那麼可執行文件是怎麼利用這個 3 號中斷來 debugger 的呢?其實就是運行時替換執行的內容,debugger 程序會在須要設置斷點的位置把指令內容換成 INT 3,也就是 0xCC,這就斷住了。就能夠獲取這時候的環境數據來作調試。

經過機器碼替換成 0xcc (INT 3)是把程序斷住了,但是怎麼恢復執行呢?其實也比較簡單,把當時替換的機器碼記錄下來,須要釋放斷點的時候再換回去就好了。

這就是可執行文件的 debugger 的原理了,最終仍是靠 cpu 支持的中斷機制來實現的。

中斷寄存器

上面說的 debugger 實現方式是修改內存中的機器碼的方式,但有的時候修改不了代碼,好比 ROM,這種狀況就要經過 cpu 提供的 4 箇中斷寄存器(DR0 - DR3)來作了。這種叫作硬中斷。

總之,INT 3 的軟中斷,還有中斷寄存器的硬中斷,是可執行文件實現 debugger 的兩種方式。

解釋型語言的 debugger

編譯型語言由於直接在操做系統之上執行,因此要利用 cpu 和操做系統的中斷機制和系統調用來實現 debugger。可是解釋型語言是本身實現代碼的解釋執行的,因此不須要那一套,可是實現思路仍是同樣的,就是插入一段代碼來斷住,支持環境數據的查看和代碼的執行,當釋放斷點的時候就繼續往下執行。

好比 javascript 中支持 debugger 語句,當解釋器執行到這一條語句的時候就會斷住。

解釋型語言的 debugger 相對簡單一些,不須要了解 cpu 的 INT 3 中斷。

debugger 客戶端

上面咱們瞭解了直接執行和解釋執行的代碼的 debugger 分別是怎麼實現的。咱們知道了代碼是怎麼斷住的,那麼斷住以後呢?怎麼把環境數據暴露出去,怎麼執行外部代碼?

這就須要 debugger 客戶端了。

好比 v8 引擎會把設置斷點、獲取環境信息、執行腳本的能力經過 socket 暴露出去,socket 傳遞的信息格式就是 v8 debug protocol

好比:

設置斷點:

{
    "seq":117,
    "type":"request",
    "command":"setbreakpoint",
    "arguments":{
        "type":"function",
        "target":"f"
    }
複製代碼

去掉斷點:

{
    "seq":117,
    "type":"request",
    "command":"clearbreakpoint",
    "arguments": {
        "type":"function",
        "breakpoint":1
     }
}
複製代碼

繼續:

{
    "seq":117,
    "type":"request",
    "command":"continue"
}
複製代碼

執行代碼:

{
    "seq":117,
    "type":"request",
    "command":"evaluate",
    "arguments":{
        "expression":"1+2"
    }
}
複製代碼

感興趣的同窗能夠去 v8 debug protocol 的文檔中去查看所有的協議。

基於這些協議就能夠控制 v8 的 debugger 了,全部的可以實現 debugger 的都是對接了這個協議,好比 chrome devtools、vscode debugger 還有其餘各類 ide 的 debugger。

nodejs 代碼的調試

nodejs 能夠經過添加 --inspect 的 option 來作調試(也能夠是 --inspect-brk,這個會在首行就斷住)。

它會起一個 debugger 的 websocket 服務端,咱們能夠用 vscode 來調試 nodejs 代碼,也能夠用 chrome devtools 來調試(見 nodejs debugger 文檔)。

➜ node --inspect test.js
Debugger listening on ws://127.0.0.1:9229/db309268-623a-4abe-b19a-c4407ed8998d
For help see https://nodejs.org/en/docs/inspector
複製代碼

原理就是實現了 v8 debug protocol。

咱們若是本身作調試工具、作 ide,那就要對接這個協議。

debugger adaptor protocol

上面介紹的 v8 debug protocol 能夠實現 js 代碼的調試,那麼 python、c# 等確定也有本身的調試協議,若是要實現 ide,都要對接一遍太過麻煩。因此後來出現了一箇中間層協議,DAP(debugger adaptor protocol)。

debugger adaptor protocol, 顧名思義,就是適配的,一端適配各類 debugger 協議,一端提供給客戶端統一的協議。這是適配器模式的一個很好的應用。

總結

本文咱們學習了 debugger 的實現原理和暴露出的調試協議。

首先咱們瞭解了代碼兩種運行方式:直接執行和解釋執行,而後分析了下爲何須要 debugger。

以後探索了直接執行的代碼經過 INT 3 的中斷的方式來實現 debugger 和解釋型語言本身實現的 debugger。

而後 debugger 的能力會經過 socket 暴露給客戶端,提供調試協議,好比 v8 debug protocol,各類客戶端包括 chrome devtools、ide 等都實現了這個協議。

可是每種語言都要實現一次的話太過麻煩,因此後來出現了一個適配層協議,屏蔽了不一樣協議的區別,提供統一的協議接口給客戶端用。

但願這篇文章可以讓你理解 debugger 的原理,若是要實現調試工具也知道怎麼該怎麼去對接協議。可以知道 chrome devtools、vscode 爲啥均可以調試 nodejs 代碼。

相關文章
相關標籤/搜索