<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JavaScript Debugger 原理揭秘

          共 5287字,需瀏覽 11分鐘

           ·

          2021-05-25 14:24

          代碼寫完會運行一下看下效果,開發(fā)的時候我們更多都是通過 dubugger 來單步或斷點運行。我們整天在用 debugger,可是你有想過它的實現原理么。

          本文會解答以下問題:

          • 代碼運行的底層原理是什么
          • 為什么需要 debugger
          • debugger 實現原理是什么
          • 如何實現 debugger 客戶端

          代碼運行的原理是什么

          代碼的運行方式可以分為直接執(zhí)行和解釋執(zhí)行兩類。

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

          直接執(zhí)行

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

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

          編譯型語言會經過編譯、匯編、鏈接的階段,編譯是把源代碼轉成匯編語言構成的中間代碼,匯編是把中間代碼變成目標代碼,鏈接會把目標代碼組合成可執(zhí)行文件。這個可執(zhí)行文件是可以在操作系統(tǒng)上直接執(zhí)行的。就因為它是由 cpu 的機器指令構成的,可以直接控制 cpu。所以可以直接 ./xxx 就可以執(zhí)行。

          解釋執(zhí)行

          編譯型語言都是生成可執(zhí)行文件直接在操作系統(tǒng)上來執(zhí)行的,不需要安裝解釋器,而 js、python 等解釋型語言的代碼需要用解釋器來跑。

          為什么有了解釋器就不需要生成機器碼了,cpu 仍然不認識這些代碼?。?/p>

          那是因為解釋器是需要編譯成機器碼的,cpu 知道怎么執(zhí)行解釋器,而解釋器知道怎么執(zhí)行更上層的腳本代碼,就這樣,由機器碼解釋執(zhí)行解釋器,再由解釋器解釋執(zhí)行上層代碼,這就是腳本語言的原理。 包括 js、python 等都是這樣。

          但是解釋器畢竟多了一層,所以有的時候會把它編譯成機器碼來直接執(zhí)行,這就是 JIT 編譯器。比如 js 引擎一般就是由 parser、解釋器、JIT 編譯器、GC 構成,大部分代碼是由解釋器解釋執(zhí)行的,而熱點代碼會經過 JIT 編譯器編譯成由機器碼,直接在操作系統(tǒng)上執(zhí)行以提高性能。

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

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

          為什么需要 debugger

          我們知道,圖靈完備的語言可以解釋任何可計算問題,所以不管是編譯型還是解釋型都能夠描述所有可計算的業(yè)務邏輯。

          我們利用不同的語言描述業(yè)務邏輯,然后運行它看效果,當代碼的邏輯比較復雜的時候,難免會出錯,我們希望能夠一步步運行或是運行到某個點停下來,然后看一下當時的環(huán)境中的變量,執(zhí)行某個腳本。完成這個功能的就是 debugger。

          也許還有很多初級程序員只會用 console.log 打日志,但是日志不能完全展現當時的環(huán)境,最好的方式還是 debugger。

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

          debugger 的原理

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

          可執(zhí)行文件的 debugger

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

          中斷

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

          INT 指令

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

          那么可執(zhí)行文件是怎么利用這個 3 號中斷來 debugger 的呢?其實就是運行時替換執(zhí)行的內容,debugger 程序會在需要設置斷點的位置把指令內容換成 INT 3,也就是 0xCC,這就斷住了。就可以獲取這時候的環(huán)境數據來做調試。

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

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

          中斷寄存器

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

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

          解釋型語言的 debugger

          編譯型語言因為直接在操作系統(tǒng)之上執(zhí)行,所以要利用 cpu 和操作系統(tǒng)的中斷機制和系統(tǒng)調用來實現 debugger。但是解釋型語言是自己實現代碼的解釋執(zhí)行的,所以不需要那一套,但是實現思路還是一樣的,就是插入一段代碼來斷住,支持環(huán)境數據的查看和代碼的執(zhí)行,當釋放斷點的時候就繼續(xù)往下執(zhí)行。

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

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

          debugger 客戶端

          上面我們了解了直接執(zhí)行和解釋執(zhí)行的代碼的 debugger 分別是怎么實現的。我們知道了代碼是怎么斷住的,那么斷住之后呢?怎么把環(huán)境數據暴露出去,怎么執(zhí)行外部代碼?

          這就需要 debugger 客戶端了。

          比如 v8 引擎會把設置斷點、獲取環(huán)境信息、執(zhí)行腳本的能力通過 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
               }
          }

          繼續(xù):

          {
              "seq":117,
              "type":"request",
              "command":"continue"
          }

          執(zhí)行代碼:

          {
              "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,這個會在首行就斷?。?/p>

          它會起一個 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 協議,一端提供給客戶端統(tǒng)一的協議。這是適配器模式的一個很好的應用。

          總結

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

          首先我們了解了代碼兩種運行方式:直接執(zhí)行和解釋執(zhí)行,然后分析了下為什么需要 debugger。

          之后探索了直接執(zhí)行的代碼通過 INT 3 的中斷的方式來實現 debugger 和解釋型語言自己實現的 debugger。

          然后 debugger 的能力會通過 socket 暴露給客戶端,提供調試協議,比如 v8 debug protocol,各種客戶端包括 chrome devtools、ide 等都實現了這個協議。

          但是每種語言都要實現一次的話太過麻煩,所以后來出現了一個適配層協議,屏蔽了不同協議的區(qū)別,提供統(tǒng)一的協議接口給客戶端用。

          希望這篇文章能夠讓你理解 debugger 的原理,如果要實現調試工具也知道怎么該怎么去對接協議。能夠知道 chrome devtools、vscode 為啥都可以調試 nodejs 代碼。

          愛心三連擊

          1.看到這里了就點個在看支持下吧,你的在看是我創(chuàng)作的動力。

          2.關注公眾號腦洞前端,獲取更多前端硬核文章!加個星標,不錯過每一條成長的機會。

          3.如果你覺得本文的內容對你有幫助,就幫我轉發(fā)一下吧。

          瀏覽 42
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  婷婷五月天亚洲无码 | 伊人久久久久亚洲AV无码裤子 | 免费黄色视频网站大全 | 精品国产探花在线观看 | 伊人视屏 |