<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>

          Linux 內核調試利器 | kprobe 的使用

          共 9153字,需瀏覽 19分鐘

           ·

          2021-07-19 03:00

          軟件調試 是軟件開發(fā)中一個必不可少的過程,通過軟件調試可以排查系統(tǒng)中存在的 BUG。我們在開發(fā)應用層程序時,可以使用 GDB 對程序進行調試。但由于 GDB 只能調試應用層程序,并不能用于調試內核代碼。

          那么,如何調試內核代碼呢?與調試應用層程序的 GDB 類似,調試內核代碼也有個名叫 KGDB 的工具,但是使用起來比較繁瑣。所以,本文將會介紹一個使用起來比較簡單的內核調試工具:kprobe

          本篇文章主要介紹 kprobe 的使用,下篇文章將會介紹 kprobe 的實現(xiàn)原理。

          kprobe 簡介

          回憶一下我們在開發(fā)應用程序時是怎樣調試代碼的?最原始的方法就是,在代碼中使用 printf 這類打印函數(shù)把結果輸出到屏幕或者日志中。當然在內核中有類似的打印函數(shù):printk,但使用 printk 函數(shù)調試內核代碼的話,必須要重新編譯 Linux 內核代碼,代價非常高。

          所以,內核開發(fā)者們開發(fā)出一種不需要重新編譯內核代碼的調試工具:kprobe。

          kprobe 可以讓用戶在內核幾乎所有的地址空間或函數(shù)(某些函數(shù)是被能被探測的)中插入探測點,用戶可以在這些探測點上通過定義自定義函數(shù)來調試內核代碼。

          用戶可以對一個探測點進行執(zhí)行前和執(zhí)行后調試,在介紹 kprobe 的使用方式前,我們先來了解一下 struct kprobe 結構,其定義如下:

          struct kprobe {
              ...
              kprobe_opcode_t        *addr;
              const char             *symbol_name;
              unsigned int           offset;

              kprobe_pre_handler_t   pre_handler;
              kprobe_post_handler_t  post_handler;
              kprobe_fault_handler_t fault_handler;
              ...
          };

          一個 struct kprobe 結構表示一個探測點,下面介紹一下其各個字段的作用:

          • addr:要探測的指令所在的內存地址(由于需要知道指令的內存地址,所以比較少使用)。
          • symbol_name:要探測的內核函數(shù),symbol_name 與 addr 只能選擇一個進行探測。
          • offset:探測點在內核函數(shù)內的偏移量,用于探測內核函數(shù)內部的指令,如果該值為0表示函數(shù)的入口。
          • pre_handler:在探測點處的指令執(zhí)行前,被調用的調試函數(shù)。
          • post_handler:在探測點處的指令執(zhí)行后,被調用的調試函數(shù)。
          • fault_handler:在執(zhí)行 pre_handlerpost_handler 或單步執(zhí)行被探測指令時出現(xiàn)內存異常,則會調用這個回調函數(shù)。

          一個 kprobe 探測點的執(zhí)行過程如下圖所示:

          從上面的介紹可知,kprobe 一般用于調試內核函數(shù)。

          kprobe 使用

          接下來,我們介紹一下怎么使用 kprobe 來調試內核函數(shù)。

          使用 kprobe 來進行內核調試的方式有兩種:

          • 第一種是通過編寫內核模塊,向內核注冊探測點。探測函數(shù)可根據(jù)需要自行定制,使用靈活方便;
          • 第二種方式是使用 kprobes on ftrace,這種方式是 kprobe 和 ftrace 結合使用,即可以通過 kprobe 來優(yōu)化 ftrace 來跟蹤函數(shù)的調用。

          由于第一種方式靈活而且功能更為強大,所以本文主要介紹第一種使用方式。

          要編寫一個 kprobe 內核模塊,可以按照以下步驟完成:

          • 第一步:根據(jù)需要來編寫探測函數(shù),如 pre_handler 和 post_handler 回調函數(shù)。
          • 第二步:定義 struct kprobe 結構并且填充其各個字段,如要探測的內核函數(shù)名和各個探測回調函數(shù)。
          • 第三步:通過調用 register_kprobe 函數(shù)注冊一個探測點。
          • 第四步:編寫 Makefile 文件。
          • 第五步:編譯并安裝內核模塊。

          接下來就按照上面的步驟來完成一個 kprobe 的內核模塊。

          1. 定義回調函數(shù)

          第一步就是編寫追蹤的回調函數(shù),一般來說只需要編寫 pre_handler、post_handler 和 fault_handler 這三個回調函數(shù),當然也可以只編寫你想追蹤的其中某一個回調函數(shù)。下面我們將會完成這三個追蹤回調函數(shù)的編寫:

          pre_handler 回調函數(shù)

          我們首先編寫要追蹤的內核函數(shù)被調用前的回調函數(shù) pre_handler,代碼如下:

          static int pre_handler(struct kprobe *p, struct pt_regs *regs)
          {
              printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx, flags = 0x%lx\n",
                     p->addr, regs->ip, regs->flags);
              return 0;
          }

          上面的函數(shù)只是簡單的打印了要追蹤的內核函數(shù)的內存地址、ip 寄存器和 flags 寄存器的值,在函數(shù)的定義中可以發(fā)現(xiàn)有個類型為 pt_regs 結構的參數(shù) ,其主要保存了 CPU 各個寄存器的值,不同 CPU 架構的定義不一樣,例如 x86 CPU 架構的定義如下:

          struct pt_regs {
              long ebx;        // ebx寄存器
              long ecx;        // ecx寄存器
              long edx;        // edx寄存器
              long esi;        // esi寄存器
              long edi;        // edi寄存器
              long ebp;        // ebp寄存器
              long eax;        // eax寄存器
              int  xds;        // ds寄存器
              int  xes;        // es寄存器
              int  xfs;        // fs寄存器
              long orig_eax;   // ...
              long eip;        // eip寄存器
              int  xcs;        // cs寄存器
              long eflags;     // eflags寄存器
              long esp;        // esp寄存器
              int  xss;        // ss寄存器
          };

          所以我們可以通過這個結構來獲取 CPU 各個寄存器的值。

          post_handler 回調函數(shù)

          接著我們來編寫要追蹤的內核函數(shù)被調用后的回調函數(shù) post_handler,其代碼如下:

          static void 
          post_handler(struct kprobe *p, struct pt_regs *regs, unsigned long flags)
          {
              printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
                     p->addr, regs->flags);
          }

          post_handler 回調函數(shù)也只是簡單的打印了要追蹤的內核函數(shù)的內存地址和 flags 寄存器的值。

          fault_handler 回調函數(shù)

          最后我們來編寫當發(fā)生內存異常時的回調函數(shù) fault_handler,其代碼如下:

          static int fault_handler(struct kprobe *p, struct pt_regs *regs, int trapnr)
          {
              printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
                     p->addr, trapnr);
              return 0;
          }

          fault_handler 回調函數(shù)打印了要追蹤的內核函數(shù)的內存地址和發(fā)生異常時的異常編號。

          2. 定義 kprobe 結構

          接下來我們定義一個 struct kprobe 結構并且填充其各個字段值,代碼如下:

          static struct kprobe kp = {
              .symbol_name   = "do_fork",      // 要追蹤的內核函數(shù)為 do_fork
              .pre_handler   = pre_handler;    // pre_handler 回調函數(shù)
              .post_handler  = post_handler;   // post_handler 回調函數(shù)
              .fault_handler = fault_handler;  // fault_handler 回調函數(shù)
          };

          由于我們要追蹤 do_fork 內核函數(shù),所以在 kprobe 結構的 symbol_name 設置為 do_fork 字符串,然后設置各個回調函數(shù)即可。

          3. 注冊追蹤點

          最后通過調用 register_kprobe 函數(shù)來注冊追蹤點,代碼如下:

          static int __init kprobe_init(void)
          {
              int ret;

              ret = register_kprobe(&kp); // 調用 register_kprobe 注冊追蹤點
              if (ret < 0) {
                  printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
                  return ret;
              }
              printk(KERN_INFO "planted kprobe at %p\n", kp.addr);
              return 0;
          }

          static void __exit kprobe_exit(void)
          {
              unregister_kprobe(&kp); // 調用 unregister_kprobe 注銷追蹤點
              printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
          }

          module_init(kprobe_init) // 注冊模塊初始化函數(shù)
          module_exit(kprobe_exit) // 注冊模塊退出函數(shù)
          MODULE_LICENSE("GPL");

          4. 編寫 Makefile 文件

          Makefile 文件用于編譯內核模塊時使用,一般來說編譯內核模塊的 Makefile 格式相對固定,如下:

          obj-m := kprobe_example.o # 編譯后的二進制文件名
           
          CROSS_COMPILE=''
          KDIR := /lib/modules/$(shell uname -r)/build
          all:
                  make -C $(KDIR) M=$(PWD) modules 
          clean:
                  rm -f *.ko *.o *.mod.o *.mod.c .*.cmd *.symvers  modul*

          5. 編譯并安裝內核模塊

          最后,我們編譯并且安裝這個內核模塊,命令如下:

          $ make
          $ sudo insmod kprobe_example.ko

          安裝完成后,隨便敲入一個命令(如 ls),然后通過調用 dmesg 命令查看內核模塊輸出的結果,如下所示:

          ...
          planted kprobe at ffffffff81076400
          pre_handler: p->addr = 0xffffffff81076400, ip = ffffffff81076401, flags = 0x246
          post_handler: p->addr = 0xffffffff81076400, flags = 0x246
          pre_handler: p->addr = 0xffffffff81076400, ip = ffffffff81076401, flags = 0x246
          post_handler: p->addr = 0xffffffff81076400, flags = 0x246
          pre_handler: p->addr = 0xffffffff81076400, ip = ffffffff81076401, flags = 0x246
          post_handler: p->addr = 0xffffffff81076400, flags = 0x246

          可以看出,我們的調試模塊已經(jīng)正常工作,并且輸出我們需要的信息。

          總結

          本文主要介紹了 kprobe 的使用方式,kprobe 的功能非常強大,可以幫助我們發(fā)現(xiàn)內核的一些 BUG。當然,本文也只是非常簡單的介紹其使用,但有了這些基礎就可以完成很多復雜的調試。

          本文主要為了接下來的 kprobe 原理與實現(xiàn)打好基礎,一下篇文章將會介紹 kprobe 的原理和實現(xiàn),有興趣的同學多多關注。


          瀏覽 205
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  91无码人妻一区二区成人aⅴ | 亚洲一二三四五区 | 青草视频在线观看免费 | 成人一区二区三区四区五区91电影 | 国产又爽 又黄 免费视频两年半 |