1. 程式人生 > 其它 >kmem 反編譯linux核心_Linux核心適配的一則小記

kmem 反編譯linux核心_Linux核心適配的一則小記

技術標籤:kmem 反編譯linux核心

d6fcf635-4c24-eb11-8da9-e4434bdf6706.png

我們的產品包含多個核心驅動模組,隨著Linux核心的不斷演進,既有的驅動程式碼可能因為使用了一些被新版本核心所廢棄的函式或者資料結構,導致不能編譯通過,或者執行時出錯。這時,我們就需要修改我們的驅動程式碼,以便其能在新版本的核心上正常工作,這個過程通常被稱為「適配」。

最近就接到了一個客戶在CentOS 7上適配5.7.x核心的需求,在此之前,我們適配過的最高核心版本是5.4.x。經過與Makefile的一番較勁,編譯總算通過了,可是一執行新編譯的驅動,系統立刻卡住,失去響應。

本來想著要是有coredump檔案的話,可以好好分析下,後來才意識到,像這種非CentOS標準標本的核心(比如CentOS 7.8對應的標準核心是3.10.0-1127),網上是沒有對應的debuginfo可供下載的,就算產生了coredump也是白搭。不過好在還是有一段彌足珍貴的call trace:

d7fcf635-4c24-eb11-8da9-e4434bdf6706.png

從"Comm"後面跟的名字看,確實是我們產品中操作這個驅動的應用層程序的名稱。這裡顯然是在做一個"open"操作,沒有debuginfo,不知道具體開啟的是哪個檔案,不過從"proc_reg_open"來看,應該是在訪問"/proc"目錄下的某個檔案。

我們的驅動會在procfs檔案系統中註冊一些引數,以便於觀察該驅動的狀態,以及動態地調整一部分配置。為了確定是由訪問這些引數引起的異常,嘗試手動載入對應的驅動,然後去訪問"/proc"目錄下我們驅動生成的檔案,果然在"cat"其中一個檔案時,復現了這個問題。

call trace給出的錯誤提示是訪問了一個空指標,並且是在呼叫mutex_lock()的時候。沒有5.7.x版本的debuginfo,那就退而求其次,藉助一個擁有除錯資訊、並且版本儘可能接近的核心來輔助分析。在基於4.18核心的CentOS 8上,先來用crash工具看一下"mutex_lock"的反彙編:

dafcf635-4c24-eb11-8da9-e4434bdf6706.png

結合對應原始碼可以推斷,多半就是因為這裡的"lock"被判定為了空指標:

bool __mutex_trylock_fast(struct mutex *lock)
{
    if (atomic_long_try_cmpxchg_acquire(&lock->owner, &zero, curr))
        return true;
...

"lock"是上一級函式傳入的引數,所以得再順著call trace往前找找:

loff_t seq_lseek(struct file *file, loff_t offset, int whence)
{
    struct seq_file *m = file->private_data;
    loff_t retval = -EINVAL;

    mutex_lock(&m->lock);
...

看來是"file->private_data->lock"為空指標,在crash工具中用"struct"命令查一下:

dbfcf635-4c24-eb11-8da9-e4434bdf6706.png

再結合seg_lseek()的反彙編結果:

dcfcf635-4c24-eb11-8da9-e4434bdf6706.png

"rdi"暫存器的值加上"0xc8"得到一個記憶體地址,將該地址處的變數的值賦給"rbp"(注意這裡是AT&T彙編語法,而不是Intel彙編語法),然後計算出"rbp"加上"0x40"的地址送給"r14",所以這裡前一個"rdi"儲存的值就是"file",而"rbp"儲存的值就是"private_data"(即"seq_file")。

因為我們借用的是4.18的核心來反彙編,對照原始碼,在5.7核心中,"seq_file"結構體中"lock"的偏移是0x38(4.18核心是0x40),而在call trace裡顯示的空指標引用就是"0x38",說明作為基址的"file->private_data"的值為0。

那這個"private_data"為什麼會為0呢?繼續從更上一級caller中尋找線索。依然是結合反彙編和原始碼,推斷是從這裡進入下一級函式的:

int proc_reg_open(struct inode *inode, struct file *file)
{
    open = pde->proc_ops->proc_open;
    if (open)
        rv = open(inode, file);
...

"open"實際會呼叫我們驅動註冊的"xxx_open",而"xxx_open"最終又會調到"seq_open"。

struct file_operations xxx_fops = {
    .owner = THIS_MODULE,
    .open = xxx_open,
    .llseek = seq_lseek,
...

int xxx_open(struct inode *inode, struct file *file)
{
    int rc = seq_open(file, &yyy_ops);
...

看下seq_open()函式的實現:

int seq_open(struct file *file, const struct seq_operations *op)
{
    struct seq_file *p = kmem_cache_zalloc(seq_file_cache, GFP_KERNEL);
    if (!p)
        return -ENOMEM;

    file->private_data = p;

    mutex_init(&p->lock);
...

如果呼叫了"seq_open",那麼"p"值就不應該為0,否則就會因為"ENOMEM"返回了,而且往下走的話,mutex_init()完成初始化操作,後面再使用這個mutex lock的時候,也就不應該再出現NULL pointer的問題。

而且,反覆檢視這一段呼叫路徑,也沒發現從哪裡可能進入"seq_lseek",從"open"怎麼會直接調到"lseek"裡去呢?筆者甚至都開始懷疑這段call stack打錯了……

分析陷入僵局,只能從其他地方尋找突破口了。既然我們之前支援過5.4.x的核心,那來對比下這兩個版本的核心在原始碼上的差異,看看到底是哪一處變動導致的。根據最新的統計資料,核心平均每1小時就有超過10次commit,所以肯定得結合這個call trace,來縮小差異對比的範圍。

在兩個版本的原始碼中,"mutex_lock"的實現沒有變化,"seq_lseek"在呼叫"mutex_lock"前的這部分實現也是完全相同的,到了"proc_reg_open"這裡,粗看好像也是沒有大的差異,但仔細一瞧,還是察覺出了一絲端倪:

defcf635-4c24-eb11-8da9-e4434bdf6706.png

在5.4.x的核心中,"proc_dir_entry"結構體的定義是這樣的:

struct proc_dir_entry {
    const struct file_operations *proc_fops;
...

而到了5.7.x版本,對應的部分則變成了這樣:

    union {
        const struct proc_ops *proc_ops;
        const struct file_operations *proc_dir_ops;
    };

這個有趣的"union"啊,當我們的驅動還是用"file_operations"去註冊時,編譯階段是不會報錯的,但到了執行階段,根據"pde->proc_ops->proc_open",實際是按照"proc_ops"來呼叫的:

dffcf635-4c24-eb11-8da9-e4434bdf6706.png

根據C語言"union"的元素共享記憶體地址的屬性,本來是想調"proc_open",結果地址指向的是"llseek",可不就走到seq_lseek()裡去了嘛,沒有"seq_open"進行初始化就呼叫了"seq_lseek",可不就是會出現指標訪問的地址異常麼。

看似詭異的一段call trace,原來隱藏著這樣的玄機。表象上是mutex lock的時候訪問到了空指標,root cause卻是因為前面2級函式中結構體中元素的變化,以及"union"的特殊性。

那核心開發者為什麼要做出這樣一個程式碼的改動呢?進入Linux的github,用"Blame"模式看下這個改動是由哪次commit引入的:

e0fcf635-4c24-eb11-8da9-e4434bdf6706.png

根據這次提交的說明,之前procfs一直通過"file_operations"來讓模組掛接自定義的函式,而"file_operations"本身是設計給VFS用的,但進入procfs的呼叫路徑後,其實跟VFS已經沒有什麼直接的關係了。

借用人家的結構體來用看起來是省事,但每當VFS擴充套件自己的"file_operations"時,procfs也必須跟著擴充套件。VFS裡大部分的operations對procfs來說都是用不到的,這樣白白增加空間佔用,實在說不過去。

Currently core /proc code uses "struct file_operations" for custom hooks,
however, VFS doesn't directly call them. Every time VFS expands
file_operations hook set, /proc code bloats for no reason.
Introduce "struct proc_ops" which contains only those hooks which /proc
allows to call into (open, release, read, write, ioctl, mmap, poll). It
doesn't contain module pointer as well.

所以呢,procfs的維護者終於決定設計一套適合自己的operations函式,實際用到哪些,才會包含哪些,也就是這個新的"proc_ops"結構體啦。

參考:

  • https://en.wikipedia.org/wiki/X86_assembly_language
  • What's the purpose of the LEA instruction

原創文章,轉載請註明出處。