Linux源碼分析之Ptrace

本文摘自互聯網,如有侵權,請聯繫我。

一、函數說明

1.函數使用說明

名字

ptrace – 進程跟蹤

形式

#include <sys/ptrace.h> 
int ptrace(int request, int pid, int addr, int data); 

描述

Ptrace 提供了一種父進程可以控制子進程運行,並可以檢查和改變它的核心image。它主要用於實現斷點調試。一個被跟蹤的進程運行中,直到發生一個信號。則進程被中止,並且通知其父進程。在進程中止的狀態下,進程的內存空間可以被讀寫。父進程還可以使子進程繼續執行,並選擇是否是否忽略引起中止的信號。

Request參數決定了系統調用的功能:

請求 作用
PTRACE_TRACEME 本進程被其父進程所跟蹤。其父進程應該希望跟蹤子進程。
PTRACE_PEEKTEXT, PTRACE_PEEKDATA 從內存地址中讀取一個字節,內存地址由addr給出。
PTRACE_PEEKUSR 從USER區域中讀取一個字節,偏移量爲addr。
PTRACE_POKETEXT, PTRACE_POKEDATA 往內存地址中寫入一個字節。內存地址由addr給出。
PTRACE_POKEUSR 往USER區域中寫入一個字節。偏移量爲addr。
PTRACE_SYSCALL, PTRACE_CONT 重新運行。
PTRACE_KILL 殺掉子進程,使它退出。
PTRACE_SINGLESTEP 設置單步執行標誌
PTRACE_ATTACH 跟蹤指定pid 進程。
PTRACE_DETACH 結束跟蹤

Intel386特有:

請求 作用
PTRACE_GETREGS 讀取寄存器
PTRACE_SETREGS 設置寄存器
PTRACE_GETFPREGS 讀取浮點寄存器
PTRACE_SETFPREGS 設置浮點寄存器

init進程不可以使用此函數

返回值
成功返回0。錯誤返回-1。errno被設置。

錯誤
EPERM
特殊進程不可以被跟蹤或進程已經被跟蹤。
ESRCH
指定的進程不存在
EIO
請求非法

2.功能詳細描述

1)PTRACE_TRACEME

形式:ptrace(PTRACE_TRACEME,0 ,0 ,0)
描述:本進程被其父進程所跟蹤。其父進程應該希望跟蹤子進程。

2)PTRACE_PEEKTEXT, PTRACE_PEEKDATA

形式:ptrace(PTRACE_PEEKTEXT, pid, addr, data)
ptrace(PTRACE_PEEKDATA, pid, addr, data)
描述:從內存地址中讀取一個字節,pid表示被跟蹤的子進程,內存地址由addr給出,data爲用戶變量地址用於返回讀到的數據。在Linux(i386)中用戶代碼段與用戶數據段重合所以讀取代碼段和數據段數據處理是一樣的。

3)PTRACE_POKETEXT, PTRACE_POKEDATA

形式:ptrace(PTRACE_POKETEXT, pid, addr, data)
ptrace(PTRACE_POKEDATA, pid, addr, data)
描述:往內存地址中寫入一個字節。pid表示被跟蹤的子進程,內存地址由addr給出,data爲所要寫入的數據。

4)PTRACE_PEEKUSR

形式:ptrace(PTRACE_PEEKUSR, pid, addr, data)
描述:從USER區域中讀取一個字節,pid表示被跟蹤的子進程,USER區域地址由addr給出,data爲用戶變量地址用於返回讀到的數據。USER結構爲core文件的前面一部分,它描述了進程中止時的一些狀態,如:寄存器值,代碼、數據段大小,代碼、數據段開始地址等。在Linux(i386)中通過PTRACE_PEEKUSER和PTRACE_POKEUSR可以訪問USER結構的數據有寄存器和調試寄存器。

5)PTRACE_POKEUSR

形式:ptrace(PTRACE_POKEUSR, pid, addr, data)
描述:往USER區域中寫入一個字節,pid表示被跟蹤的子進程,USER區域地址由addr給出,data爲需寫入的數據。

6)PTRACE_CONT

形式:ptrace(PTRACE_CONT, pid, 0, signal)
描述:繼續執行。pid表示被跟蹤的子進程,signal爲0則忽略引起調試進程中止的信號,若不爲0則繼續處理信號signal。

7)PTRACE_SYSCALL

形式:ptrace(PTRACE_SYS, pid, 0, signal)
描述:繼續執行。pid表示被跟蹤的子進程,signal爲0則忽略引起調試進程中止的信號,若不爲0則繼續處理信號signal。與PTRACE_CONT不同的是進行系統調用跟蹤。在被跟蹤進程繼續運行直到調用系統調用開始或結束時,被跟蹤進程被中止,並通知父進程。

8)PTRACE_KILL

形式:ptrace(PTRACE_KILL,pid)
描述:殺掉子進程,使它退出。pid表示被跟蹤的子進程。

9)PTRACE_SINGLESTEP

形式:ptrace(PTRACE_KILL, pid, 0, signle)
描述:設置單步執行標誌,單步執行一條指令。pid表示被跟蹤的子進程。signal爲0則忽略引起調試進程中止的信號,若不爲0則繼續處理信號signal。當被跟蹤進程單步執行完一個指令後,被跟蹤進程被中止,並通知父進程。

10)PTRACE_ATTACH

形式:ptrace(PTRACE_ATTACH,pid)
描述:跟蹤指定pid 進程。pid表示被跟蹤進程。被跟蹤進程將成爲當前進程的子進程,並進入中止狀態。

11)PTRACE_DETACH

形式:ptrace(PTRACE_DETACH,pid)
描述:結束跟蹤。 pid表示被跟蹤的子進程。結束跟蹤後被跟蹤進程將繼續執行。

12)PTRACE_GETREGS

形式:ptrace(PTRACE_GETREGS, pid, 0, data)
描述:讀取寄存器值,pid表示被跟蹤的子進程,data爲用戶變量地址用於返回讀到的數據。此功能將讀取所有17個基本寄存器的值。

13)PTRACE_SETREGS

形式:ptrace(PTRACE_SETREGS, pid, 0, data)
描述:設置寄存器值,pid表示被跟蹤的子進程,data爲用戶數據地址。此功能將設置所有17個基本寄存器的值。

14)PTRACE_GETFPREGS

形式:ptrace(PTRACE_GETFPREGS, pid, 0, data)
描述:讀取浮點寄存器值,pid表示被跟蹤的子進程,data爲用戶變量地址用於返回讀到的數據。此功能將讀取所有浮點協處理器387的所有寄存器的值。

15)PTRACE_SETFPREGS

形式:ptrace(PTRACE_SETREGS, pid, 0, data)
描述:設置浮點寄存器值,pid表示被跟蹤的子進程,data爲用戶數據地址。此功能將設置所有浮點協處理器387的所有寄存器的值。

二、80386的調試設施

80386提供的調試設施包括:
- 字節的陷阱指令
- 單步指令
- 斷點檢測
- 任務切換時的自陷

1.調試斷點

斷點設施是80386爲調試程序提供的最重要的功能。

一個斷點,允許編程人員對特定的線性地址設置特定的條件;當程序訪問到該線性地址並滿足特定的條件時,即跳轉到異常處理程序。80386可支持同時設置四個斷點條件,編程人員可在程序中的四個位置設置條件,使其轉向異常處理程序。這四個斷點的每一個斷點,都可以是如下三種不同類型的任何一種:

只在指令地址與斷點地址一致時,斷點有效。
數據寫入地址與斷點地址一致時,斷點有效。
數據讀出地址或數據寫入地址與斷點地址一致時,斷點有效。

1)調試寄存器

爲支持提供四個調試斷點,在80386中增加了八個寄存器,編號爲DR0至DR7。這八個寄存器中由四個用於斷點,兩個用於控制,另兩個保留未用。對這八個寄存器的訪問,只能在0級特權級進行。在其它任何特權級對這八個寄存器中的任意一個寄存器進行讀或寫訪問,都將產生無效操作碼異常。此外,這八個寄存器還可用DR6及DR7中的BD位和GD位進行進一步的保護,使其即使是在0級也不能進行讀出或寫入。

對這些寄存器的訪問使用通常的MOV指令:

MOV reg Dri

該指令將調試寄存器i中的內容讀至通用寄存器reg中;

MOV Dri reg

這裏寫圖片描述

圖表示了這八個調試寄存器。這些寄存器的功能如下:

DR0—DR3 寄存器DR0—DR3包含有與四個斷點條件的每一個相聯繫的線性地址(斷點條件則在DR7中)。因爲這裏使用的是線性地址,所以,斷點設施的操作,無論分頁機制是否啓用,都是相同的。

DR4—DR5 保留。

DR6 DR6是調試狀態寄存器。當一個調試異常產生時,處理器設置DR6的相應位,用於指示調試異常發生的原因,幫助調試異常處理程序分析、判斷,以及作出相應處理。

DR7 DR7是調試控制寄存器。分別對應四個斷點寄存器的控制位,對斷點的啓用及斷點類型的選擇進行控制。所有斷點寄存器的保護也在此寄存器中規定。

DR6各位的功能
B0—B3 當斷點線性地址寄存器規定的條件被檢測到時,將對應的B0—B3位置1。置位B0—B3與斷點條件是否被啓用無關。即B0—B3的某位被置1,並不表示要進行對應的斷點異常處理。

BD 如下一條指令要對八個調試寄存器之一進行讀或寫時,則在指令的邊界BD位置1。在一條指令內,每當即將讀寫調試寄存器時,也BD位置1。BD位置1與DR7中GD位啓用與否無關。

BS 如果單步異常發生時,BS位被置1。單步條件由EFLAGS寄存器中的TF位啓用。如果程序由於單步條件進入調試處理程序,則BS位被置1。與DR6中的其它位不同的是,BS位只在單步陷阱實際發生時才置位,而不是檢測到單步條件就置位。

BT BT位對任務切換導致TSS中的調試陷阱位被啓用而造成的調試異常,指示其原因。對這一條件,在DR7中沒有啓用位。
DR6中的各個標誌位,在處理機的各種清除操作中不受影響,因此,調試異常處理程序在運行以前,應清除DR6,以避免下一次檢測到異常條件時,受到原來的DR6中狀態位的影響。

DR7各位的功能

LEN LEN爲一個兩位的字段,用以指示斷點的長度。每一斷點寄存器對應一個這樣的字段,所以共有四個這樣的字段分別對應四個斷點寄存器。LEN的四種譯碼狀態對應的斷點長度如下

LEN 說明
0 0 斷點爲一字節
0 1 斷點爲兩字節
1 0 保留
1 1 斷點爲四字節

這裏,如果斷點是多字節長度,則必須按對應多字節邊界進行對齊。如果對應斷點是一個指令地址,則LEN必須爲00

RWE RWE也是兩位的字段,用以指示引起斷點異常的訪問類型。共有四個RWE字段分別對應四個斷點寄存器,RWE的四種譯碼狀態對應的訪問類型如下

RWE 說明
0 0 指令
0 1 數據寫
1 0 保留
1 1 數據讀和寫

GE/LE GE/LE爲分別指示準確的全局/局部數據斷點。如果GE或LE被置位,則處理器將放慢執行速度,使得數據斷點準確地把產生斷點的指令報告出來。如果這些位沒有置位,則處理器在執行數據寫的指令接近執行結束稍前一點報告斷點條件。建議讀者每當啓用數據斷點時,啓用LE或GE。降低處理機執行速度除稍微降低一點性能以外,不會引起別的問題。但是,對速度要求嚴格的代碼區域除外。這時,必須禁用GE及LE,並且必須容許某些不太精確的調試異常報告。
L0—L3/G0—G3 L0—L3及G0—G3位分別爲四個斷點寄存器的局部及全局啓用信號。如果有任一個局部或全局啓用位被置位,則由對應斷點寄存器DRi規定的斷點被啓用。

GD GD位啓用調試寄存器保護條件。注意,處理程序在每次轉入調試異常處理程序入口處清除GD位,從而使處理程序可以不受限制地訪問調試寄存器。
前述的各個L位(即LE,L0—L3)是有關任務的局部位,使調試條件只在特定的任務啓用。而各個G位(即GD,G0—G3)是全局的,調試條件對系統中的所有任務皆有效。在每次任務切換時,處理器都要清除L位。

2)斷點地址識別

LEN字段及斷點線性地址的組合,規定調試異常檢查的四個線性地址的範圍。上面已經提到,斷點線性地址必須對齊於LEN規定的多字節長度的相應長度邊界。事實上,處理器在檢查斷點時,根據LEN規定的長度,忽略線性地址的相應低位。例如,當LEN=11時,線性地址的最低兩位被忽略,即把線性地址最低兩位視爲00,因而按四字節邊界對齊。而當LEN=01時,線性地址的最低位被忽略,即把線性地址的最低位視爲0,因而按兩字節邊界對齊。

對於由斷點線性地址及LEN規定的地址範圍內類型正確的任何字節的訪問都產生異常,數據的訪問及指令的取出,都要按所有四個斷點地址範圍進行檢查。如果斷點地址範圍的任何字節匹配,訪問的類型也匹配,則斷點異常被報告。

下表給出了識別數據斷點的幾個離子,這裏假設所有斷點被啓用,而且設置了正確的訪問類型。

這裏寫圖片描述

3)代碼斷點與數據斷點的比較

指令訪問斷點與數據訪問斷點之間有如下幾點區別:

1.在RWE字段的設置不同。指令斷點,RWE=0;數據斷點,RWE≠0。
2.LEN的設置不同。指令斷點的長度只能是00即一字節;數據斷點的長度可以是1、2、4字節。由於很多指令的長度超過一字節(事實上,指令長度爲1—15字節),所以指令斷點必須設置在指令的第一個字節。

由於指令斷點在指令執行之前被報告,因此,很明顯,對該指令不能簡單的重新執行。因爲每一次新的執行都簡單地重複產生故障,所以,如果調試處理程序不禁用斷點,則這種故障就會形成無限地循環。爲解決這一問題,就需用到80386中EFLAGS的RF位。當RF位置位時,任何指令斷點都被忽略,因此,在RF位保持爲置位狀態時,指令斷點將不再起作用。但RF位的置位狀態不會長久保持。事實上,處理器的內部邏輯保證,在任何一條指令成功完成後,都將RF位清零,因此,RF位的置位狀態最多隻保持一條指令的時間。也就是說,在RF位置位後的下一條指令,指令斷點不起作用,這樣只要在重新執行指令之前,將RF置1,即可保證該指令斷點不會形成無限循環,而且,也不影響緊接的下一條指令也設置指令斷點。

RF位的置位,不是用某一個操作直接將EFLAGS的RF位置1來完成。每當進入一個故障處理程序,處理器保存中斷現場時,需把斷點等信息壓棧。當把EFLAGS寄存器壓棧時,推入棧中的EFLAGS的RF位是1,因此用IRET指令推出故障處理程序時,從棧中彈出的EFLAGS寄存器標誌位中的RF爲1,從而將RF位置位。

2.TSS中的調度陷阱

每當通過TSS發生任務切換時,TSS中的T位使調試處理程序被調用,這就爲調試程序管理某些任務的活動提供了一種方便的方法。DR6中的BT位指示對該位的檢測,DR7中對該位沒有特別的啓用位。

如果調試處理程序是通過任務門使用的,則不能設置對應TSS的調試陷阱位。否則,將發生調試處理程序的無限循環。

3.INT3

一個斷點指令提供調試程序的另一種方法。按這種方法,要求作爲斷點指令的第一個字節用INT3指令替代。因此,程序執行到預先需要的斷點處遇到斷點指令,並進入INT3處理程序。在一些使用INT3顯然不足的地方還需使用斷點寄存器。這樣的情況有:

1.由ROM提供的代碼中,不可能插入INT3指令。
2.由於使用了INT3,原來的程序代碼被修改,使執行此代碼的其它任務也被中斷。
3.INT3不能執行數據斷點。

在另外一些情況下,使用INT3則很有用:

1.單步及斷點設施僅僅進入調試程序,而對調試處理程序的調試,INT3則是唯一方便的方法。
2.代碼中可以插入任意數量的INT3指令,而斷點設施只能提供最多四個斷點。
3.早期86系列的各種型號處理器,沒有80386提供的斷點設施,INT3指令在這些處理器中是執行任何斷點的唯一方法。
概括地說,除了某些特別情況之外,建議使用INT3指令在代碼中執行斷點,保留斷點寄存器用於數據斷點。

4.程序的步進執行

單步功能對程序調試者來說,是一個方便的調試手段。通過一條一條地執行指令,對操作數據、操作指令及操作結果地觀察和分析,可以幫助調試人員判斷出執行某一指令時,是否發生了硬件錯誤,或是否軟件邏輯錯誤。80386的單步功能通過陷阱來實現。單步陷阱在EFLAGS寄存器中的TF位置位時啓用。在一條指令開始執行時,如果有TF=1,則在指令執行的末尾產生調試異常,並進入調試處理程序。在這裏,“指令開始執行時,TF=1”這一條件是重要的。有此條件的限制,使TF位置位1的指令不會產生單步陷阱。每次產生單步陷阱之後,在進入調試處理程序之前要將TF位清除。此外,在處理中斷或異常時,也清除TF位。

如果外部中斷與單步中斷同時發生,則單步中斷被優先處理,並清除TF。在調試處理程序第一條指令執行之前,如仍有懸掛的中斷請求,則響應並處理中斷。因此,中斷處理是在沒有單步啓用的情況下完成的。如果希望在中斷處理程序中使用單步功能。則需先把中斷處理程序的第一條指令設置爲斷點,當程序運行到斷點處停下來之後,再啓用單步功能。

三、代碼分析

在Linux核心源代碼中,與完成ptrace功能相關的代碼有:
sys_ptrace函數,完成ptrace系統調用的代碼。

  • 爲完成sys_ptrace功能所需調用的一些輔助函數,寄存器讀寫函數和內存讀寫函數。
  • 信號處理函數中,對被調試進程的處理(中止其運行、繼續運行)。
  • syscall_trace函數,完成了系統調用調試下的處理。
  • 調試陷阱處理(異常1處理),完成單步執行和斷點中斷處理。
  • execve系統調用中對被調試進程裝入後中止的實現。

1.sys_ptrace函數

ptrace系統調用在覈心對應的處理函數爲sys_ptrace()(/linux/arch/i386/kernel/ptrace.c)。sys_ptrace函數完成了ptrace系統調用功能。ptrace函數的總體流程如下:
這裏寫圖片描述

asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
    struct task_struct *child;
    struct user * dummy = NULL;
    unsigned long flags;
    int i, ret;

    lock_kernel();                  
    ret = -EPERM;
    if (request == PTRACE_TRACEME) {
            。。。PTRACE_TRACEME處理
    }
    ret = -ESRCH;
    read_lock(&tasklist_lock);
    child = find_task_by_pid(pid);      /*  查找task結構  */
    read_unlock(&tasklist_lock);    
    if (!child)                         /*  沒有找到task結構,所名給定pid錯誤  */
        goto out;
    ret = -EPERM;
    if (pid == 1)                       /*  init進程不能調試  */
        goto out;
    if (request == PTRACE_ATTACH) {
            。。。PTRACE_ATTACH處理
    }
    ret = -ESRCH;
    if (!(child->flags & PF_PTRACED))   /*  進程沒有被跟蹤,不能執行其它功能  */
        goto out;
    if (child->state != TASK_STOPPED) {
        if (request != PTRACE_KILL) /*  除PTRACE_KILL外的其它功能要求  */
            goto out;                   /*  要求進程狀態爲TASK_STOPPED     */
    }
    if (child->p_pptr != current)           /*  被跟蹤進程要求爲當前進程的子進程  */
        goto out;

    switch (request) {
        case PTRACE_PEEKTEXT:
        case PTRACE_PEEKDATA: {
            。。。PTRACE_PEEKTEXT,PTRACE_PEEKDATA處理
        }
        case PTRACE_PEEKUSR: {
            。。。PTRACE_PEEKUSR處理
        }
        case PTRACE_POKETEXT: 
        case PTRACE_POKEDATA:{
            。。。PTRACE_POKETEXT,PTRACE_POKEDATA處理
}
        case PTRACE_POKEUSR: {
            。。。PTRACE_POKEUSR處理
}
        case PTRACE_SYSCALL: 
        case PTRACE_CONT: 
            。。。PTRACE_SYSCALL,PTRACE_CONT處理
        }
        case PTRACE_KILL: {
            。。。PTRACE_KILL處理
        }
        case PTRACE_SINGLESTEP: {  
            。。。PTRACE_SINGLESTEP處理
        }
        case PTRACE_DETACH: 
            。。。PTRACE_DETACH處理
        }
        case PTRACE_GETREGS: 
            。。。PTRACE_GETREGS處理
          };
        case PTRACE_SETREGS: 
            。。。PTRACE_SETREGS處理
          };
        case PTRACE_GETFPREGS: 
            。。。PTRACE_GETFPREGS處理
          };
        case PTRACE_SETFPREGS: 
            。。。PTRACE_SETFPREGS處理
          };
        default:
            ret = -EIO;
            goto out;
    }
out:
    unlock_kernel();
    return ret;
}

1) PTRACE_TRACEME處理

說明:此處理使當前進程進入調試狀態。進程是否爲調試狀態由進程的標誌PF_PTRACED表示。
流程

這裏寫圖片描述

程序:

    if (request == PTRACE_TRACEME) {
        if (current->flags & PF_PTRACED)        /*  是否已經被跟蹤  */
            goto out;
        current->flags |= PF_PTRACED;           /*  設置跟蹤標誌  */
        ret = 0;
        goto out;
    }

2)PTRACE_ATTACH處理

說明:此處理設置開始調試某一進程,此進程可以是任何進程(init進程除外)。對某一進程的調試需有對這一進程操作的權限。不能調試自身進程。一個進程不能ATTACH多次。
爲完成對一個進程的調試設置,首先設置進程標誌置PF_PTRACED。再將需調試的進程設置爲當前進程的子進程。最後向它發信號SIGSTOP中止它的運行,使它進入調試狀態。
流程
這裏寫圖片描述

程序:
    if (request == PTRACE_ATTACH) {
        if (child == current)               /*  不能調試自身進程  */
            goto out;
        if ((!child->dumpable ||
            (current->uid != child->euid) ||
            (current->uid != child->suid) ||
            (current->uid != child->uid) ||
            (current->gid != child->egid) ||
            (current->gid != child->sgid) ||
            (!cap_issubset(child->cap_permitted, current->cap_permitted)) ||
            (current->gid != child->gid)) && !capable(CAP_SYS_PTRACE))
            goto out;                   /*  檢驗用戶權限  */
        if (child->flags & PF_PTRACED) /*  一個進程不能被attach多次  */
            goto out;
        child->flags |= PF_PTRACED; /*  設置進程標誌位PF_PTRACED  */

        write_lock_irqsave(&tasklist_lock, flags);
        if (child->p_pptr != current) {     /*  設置進程爲當前進程的子進程  */
            REMOVE_LINKS(child);
            child->p_pptr = current;
            SET_LINKS(child);
        }
        write_unlock_irqrestore(&tasklist_lock, flags);
        send_sig(SIGSTOP, child, 1);        /*  發送SIGSTOP信號,中止它運行  */
        ret = 0;
        goto out;
    }

3) PTRACE_PEEKTEXT,PTRACE_PEEKDATA處理

說明:在Linux(i386)中,用戶代碼段和用戶數據段是重合的所以PTRACE_PEEKTEXT,PTRACE_PEEKDATA的處理是相同的。在其它CPU或操作系統上有可能是分開的,那要分開處理。讀寫用戶段數據通過read_long()和write_long()兩個輔助函數完成,具體函數過程參見兩函數分析。
流程
這裏寫圖片描述

case PTRACE_PEEKTEXT: 
        case PTRACE_PEEKDATA: {
            unsigned long tmp;
            down(&child->mm->mmap_sem);
            ret = read_long(child, addr, &tmp);     /*  讀取數據  */
            up(&child->mm->mmap_sem);
            if (ret >= 0)
                ret = put_user(tmp,(unsigned long *) data);  /*  返回結果  */
            goto out;
        }

4)PTRACE_POKETEXT,PTRACE_POKEDATA處理

說明:與PTRACE_PEEKTEXT,PTRACE_PEEKDATA處理相反,此處理爲寫進程內存(詳見上)
流程:
這裏寫圖片描述

    case PTRACE_POKETEXT: 
        case PTRACE_POKEDATA:
            down(&child->mm->mmap_sem);
            ret = write_long(child,addr,data);      /*  修改數據  */
            up(&child->mm->mmap_sem);
            goto out;

5)PTRACE_PEEKUSR處理

說明:在Linux(i386)中,讀寫USER區域的數據值有用戶寄存器和調試寄存器的值。用戶寄存器包括17個寄存器,它們分別是EBX、ECX、EDX、ESI、EDI、EBP、EAX、DS、ES、FS、GS、ORIG_EAX、EIP、CS、EFLAGS、ESP、SS。這些寄存器的讀寫由輔助函數putreg()和getreg()函數完成,具體實現參見兩函數分析。調試寄存器爲DR0—DR7。其中DR4和DR5爲系統保留的寄存器,不可以寫。DR0—DR3中的斷點地址必須在用戶的3G空間內,在覈心內存設置斷點非法。DR7中的RWE與LEN數據位必須合法(LEN≠10保留、RWE≠10保留、RWE=00時LEN=00指令斷點爲一字節)。

流程:
這裏寫圖片描述

case PTRACE_PEEKUSR: {
            unsigned long tmp;
            ret = -EIO;
            if ((addr & 3) || addr < 0 ||           /*  越界或字節未對齊出錯  */
                addr > sizeof(struct user) - 3)
                goto out;
            tmp = 0;  /* Default return condition */
            if(addr < 17*sizeof(long))              /*  讀取基本寄存器值  */
                tmp = getreg(child, addr);
            if(addr >= (long) &dummy->u_debugreg[0] &&
               addr <= (long) &dummy->u_debugreg[7]){
                addr -= (long) &dummy->u_debugreg[0];
                addr = addr >> 2;
                tmp = child->tss.debugreg[addr];        /*  讀取調試寄存器值  */
            };
            ret = put_user(tmp,(unsigned long *) data); /*  返回結果  */
            goto out;
        }

6)PTRACE_POKEUSR處理

說明:與PTRACE_PEEKUSR處理相反,此處理爲寫USER區域(詳見上)。
流程:
這裏寫圖片描述

        case PTRACE_POKEUSR: 
            ret = -EIO;
            if ((addr & 3) || addr < 0 ||           /*  越界或字節未對齊出錯  */
                addr > sizeof(struct user) - 3)
                goto out;
            if (addr < 17*sizeof(long)) {
                ret = putreg(child, addr, data);        /*  寫基本寄存器值  */
                goto out;
            }
          if(addr >= (long) &dummy->u_debugreg[0] &&
             addr <= (long) &dummy->u_debugreg[7]){
              if(addr == (long) &dummy->u_debugreg[4]) return -EIO;  /*  寫DR4出錯  */
              if(addr == (long) &dummy->u_debugreg[5]) return -EIO; /*  寫DR5出錯  */
              if(addr < (long) &dummy->u_debugreg[4] &&     
                 ((unsigned long) data) >= TASK_SIZE-3) return -EIO;
                                        /*  斷點地址越界出錯  */
              ret = -EIO;
              if(addr == (long) &dummy->u_debugreg[7]) { /*  寫DR7  */
                  data &= ~DR_CONTROL_RESERVED;
                  for(i=0; i<4; i++)
                      if ((0x5f54 >> ((data >> (16 + 4*i)) & 0xf)) & 1)
                          goto out; /*  LEN RWE非法出錯  */
              };
              addr -= (long) &dummy->u_debugreg;
              addr = addr >> 2;
              child->tss.debugreg[addr] = data; /*  寫調試寄存器值  */
              ret = 0;
              goto out;
          };
          ret = -EIO;
          goto out;

7)PTRACE_SYSCALL,PTRACE_CONT處理

說明:PTRACE_SYSCALL和PTRACE_CONT有着相同的處理,都是讓子進程繼續運行,其區別PTRACE_SYSCALL設置了進程標誌PF_TRACESYS。這樣可以使進程在下一次系統調用開始或結束時中止運行。繼續執行要保證清除單步執行標誌。用戶參數data爲用戶提供的信號,希望子進程繼續處理此信號。如果爲0則不處理,如果不爲0則在喚醒子進程後向子進程發送此信號(在do_signal()和syscall_trace()函數中完成)。

流程
這裏寫圖片描述

    case PTRACE_SYSCALL:
        case PTRACE_CONT: 
            long tmp;
            ret = -EIO;
            if ((unsigned long) data > _NSIG)       /*  信號超過範圍  */
                goto out;
            if (request == PTRACE_SYSCALL)
                child->flags |= PF_TRACESYS;    /*  設置PF_TRACESYS標誌  */
            else
                child->flags &= ~PF_TRACESYS; /*  去除PF_TRACESYS標誌  */
            child->exit_code = data;                /*  設置繼續處理的信號  */
            tmp = get_stack_long(child, EFL_OFFSET) & ~TRAP_FLAG;
            put_stack_long(child, EFL_OFFSET,tmp);  /*  清除TF標誌  */
            wake_up_process(child);                 /*  喚醒子進程  */
            ret = 0;
            goto out;
        }

8)PTRACE_KILL處理

說明:此功能完成殺死子進程的功能。以往殺死進程只要往此進程發送SIGKILL信號。在此處理類似於PTRACE_CONT處理,只是把子進程繼續的信號設置爲SIGKILL,則喚醒子進程後,子進程會受到SIGKILL信號。
流程
這裏寫圖片描述

        case PTRACE_KILL: {
            long tmp;
            ret = 0;
            if (child->state == TASK_ZOMBIE)    /*  進程已經退出  */
                goto out;
            child->exit_code = SIGKILL;     /*  設置繼續處理的信號SIGKILL  */
            tmp = get_stack_long(child, EFL_OFFSET) & ~TRAP_FLAG;
            put_stack_long(child, EFL_OFFSET, tmp); /*  清除TF標誌  */
            wake_up_process(child);                 /*  喚醒子進程  */
            goto out;
        }

9)PTRACE_SINGLESTEP處理

說明:單步調試,子進程運行一條指令。此處理類似於PTRACE_CONT處理。不同的只是設置類單步調試標誌TF。
流程
這裏寫圖片描述

    case PTRACE_SINGLESTEP: {
            long tmp;
            ret = -EIO;
            if ((unsigned long) data > _NSIG)       /*  信號超過範圍  */
                goto out;
            child->flags &= ~PF_TRACESYS;  /*  清除PF_TRACESYS標誌  */
            if ((child->flags & PF_DTRACE) == 0) {
                child->flags |= PF_DTRACE;      /*  設置PF_DTRACE標誌  */
            }
            tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
            put_stack_long(child, EFL_OFFSET, tmp); /*  設置TF標誌  */
            child->exit_code = data;                    /*  設置繼續處理的信號  */
            wake_up_process(child);                 /*  喚醒子進程  */
            ret = 0;
            goto out;
        }

10)PTRACE_DETACH處理

說明:終止調試一個子進程。此處理與PTRACE_ATTACH處理相反。在此做了一些清理操作:清除PF_TRACESYS和PF_PTRACED進程標誌,清除TF標誌,父進程指針還原。最後喚醒此進程,讓其繼續執行。
流程
這裏寫圖片描述

case PTRACE_DETACH: { 
            long tmp;
            ret = -EIO;
            if ((unsigned long) data > _NSIG)       /*  信號超過範圍  */
                goto out;
            child->flags &= ~(PF_PTRACED|PF_TRACESYS);
                            /*  清除PF_TRACESYS和PF_PTRACED標誌  */
            child->exit_code = data;                /*  設置繼續處理的信號  */
            write_lock_irqsave(&tasklist_lock, flags);
            REMOVE_LINKS(child);
            child->p_pptr = child->p_opptr; /*  把子進程的父進程設置爲原來的  */
            SET_LINKS(child);           
            write_unlock_irqrestore(&tasklist_lock, flags);
            tmp = get_stack_long(child, EFL_OFFSET) & ~TRAP_FLAG;
            put_stack_long(child, EFL_OFFSET, tmp); /*  清除TF標誌  */
            wake_up_process(child);                 /*  喚醒子進程  */
            ret = 0;
            goto out;
        }

11)PTRACE_GETREGS處理

說明:此功能完成讀取所有的17個用戶寄存器。讀寄存器值使用函數getreg(),詳見getreg()分析。此功能爲i386特有。
流程
這裏寫圖片描述

        case PTRACE_GETREGS: { 
            if (!access_ok(VERIFY_WRITE, (unsigned *)data,
                       17*sizeof(long)))    /*  校驗用戶給定地址是否合法可寫  */
              {
                ret = -EIO;
                goto out;
              }
            for ( i = 0; i < 17*sizeof(long); i += sizeof(long) )
              {                     /*  逐個讀取寄存器值並放到用戶空間中  */
                __put_user(getreg(child, i),(unsigned long *) data);
                data += sizeof(long);
              }
            ret = 0;
            goto out;
          };

12)PTRACE_SETREGS處理

說明:此功能完成設置所有的17個用戶寄存器。寫寄存器值使用函數putreg(),詳見putreg()分析。此功能爲i386特有。
流程
這裏寫圖片描述

    case PTRACE_SETREGS: {
            unsigned long tmp;
            if (!access_ok(VERIFY_READ, (unsigned *)data,
                       17*sizeof(long)))    /*  校驗用戶給定地址是否合法可讀  */
              {
                ret = -EIO;
                goto out;
              }
            for ( i = 0; i < 17*sizeof(long); i += sizeof(long) )
              {             /*  逐個寫寄存器值  */
                __get_user(tmp, (unsigned long *) data);
                putreg(child, i, tmp);
                data += sizeof(long);
              }
            ret = 0;
            goto out;
          };
PTRACE_GETFPREG

13)PTRACE_GETFPREGS處理

說明:此功能完成讀取所有浮點寄存器。所有浮點寄存器存放於TSS中,由TSS中的i386聯合表示浮點寄存器。如果有浮點處理器,則存放硬件的寄存器內容。否則,則存放軟件模擬的寄存器內容。此功能爲i386特有。
流程
這裏寫圖片描述

    case PTRACE_GETFPREGS: { 
            if (!access_ok(VERIFY_WRITE, (unsigned *)data,
                       sizeof(struct user_i387_struct)))
              {                 /*  校驗用戶給定地址是否合法可寫  */
                ret = -EIO;
                goto out;
              }
            ret = 0;
            if ( !child->used_math ) { /*  模擬一個空的浮點處理器  */
              /* Simulate an empty FPU. */
              child->tss.i387.hard.cwd = 0xffff037f;
              child->tss.i387.hard.swd = 0xffff0000;
              child->tss.i387.hard.twd = 0xffffffff;
            }
#ifdef CONFIG_MATH_EMULATION
            if ( boot_cpu_data.hard_math ) {
#endif
                __copy_to_user((void *)data, &child->tss.i387.hard,
                        sizeof(struct user_i387_struct));
                                /*  複製浮點寄存器值(硬件)  */
#ifdef CONFIG_MATH_EMULATION
            } else {
              save_i387_soft(&child->tss.i387.soft,
                     (struct _fpstate *)data);
                                /*  複製浮點寄存器值(軟件模擬)  */
            }
#endif
            goto out;
          };

14)PTRACE_SETFPREGS處理

說明:此功能完成設置所有浮點寄存器。此功能爲i386特有。
流程這裏寫圖片描述

        case PTRACE_SETFPREGS: 
            if (!access_ok(VERIFY_READ, (unsigned *)data,
                       sizeof(struct user_i387_struct)))
              {                 /*  校驗用戶給定地址是否合法可讀  */
                ret = -EIO;
                goto out;
              }
            child->used_math = 1;   /*  設置標誌使用浮點處理器  */
#ifdef CONFIG_MATH_EMULATION
            if ( boot_cpu_data.hard_math ) {
#endif
              __copy_from_user(&child->tss.i387.hard, (void *)data,
                       sizeof(struct user_i387_struct));
                                /*  設置浮點寄存器值(硬件)  */
#ifdef CONFIG_MATH_EMULATION
            } else {
              restore_i387_soft(&child->tss.i387.soft,
                        (struct _fpstate *)data);
                                /*  設置浮點寄存器值(軟件模擬)  */
            }
#endif
            ret = 0;
            goto out;
          };

2.寄存器讀寫輔助函數

getreg() putreg()是在ptrace.c中定義的兩個輔助函數,它們完成了對被調試子進程的寄存器讀寫功能。函數中參數regno,表示寄存器的序號。定義如下:這裏寫圖片描述
進程結構中TSS存有所有的進程寄存器值,但不能使用這些寄存器的值。因爲在調試器調用ptrace()讀寫寄存器時,被調試進程必須在中止狀態,引起被調試進程中止有兩種可能:
1.接受到信號,do_signal處理中。
2.系統調用調試中斷,syscall_trace處理中。在這時被調試進程在覈心態運行,那TSS中的寄存器爲核心態運行時的寄存器狀態,而通過ptrace()讀寫的寄存器爲用戶態的寄存器狀態。所以,getreg()和putreg()不從TSS結構中讀寫寄存器值,而要通過操作核心態的堆棧(堆棧中保存有用戶態的寄存器值)來讀寫寄存器值。

當進程系統調用或時鐘中斷處理時,系統會把所有的用戶態的寄存器壓入堆棧保存,而處理完畢之後恢復寄存器的值,這些寄存器值在堆棧中的順序如下(與regno比較):
這裏寫圖片描述
其中不包含寄存器fs和gs。對這兩個寄存器的操作通過訪問TSS結構實現。
對寫寄存器,有以下的限制:

1.對ORIG_EAX寄存器不能寫。
2.對段寄存器(CS、DS、ES、FS、GS、SS)的修改,其中RPL必須爲11(優先極爲3)。
3.對標誌寄存器EFLAG中標誌IF、RF、VM、IOPL不能修改。
函數get_stack_long()和put_stack_long()爲對子進程核心堆棧的操作。

源程序與註釋如下:

static inline int get_stack_long(struct task_struct *task, int offset)
{
    unsigned char *stack;
    stack = (unsigned char *)task->tss.esp0;        /*  獲得ESP0寄存器值  */
    stack += offset;                        /*  加偏移量  */
    return (*((int *)stack));
}

static inline int put_stack_long(struct task_struct *task, int offset,
    unsigned long data)
{
    unsigned char * stack;
    stack = (unsigned char *) task->tss.esp0;   /*  獲得ESP0寄存器值  */
    stack += offset;                        /*  加偏移量  */
    *(unsigned long *) stack = data;
    return 0;
}

static int putreg(struct task_struct *child,
    unsigned long regno, unsigned long value)
{
    switch (regno >> 2) {
        case ORIG_EAX:          /*  不能讀寫EAX  */
            return -EIO;
        case FS:
            if (value && (value & 3) != 3)
                return -EIO;        /*  優先級不爲3,出錯  */
            child->tss.fs = value;  /*  通過TSS寫寄存器fs  */
            return 0;
        case GS:
            if (value && (value & 3) != 3)
                return -EIO;        /*  優先級不爲3,出錯  */
            child->tss.gs = value;  /*  通過TSS寫寄存器gs  */
            return 0;
        case DS:
        case ES:
            if (value && (value & 3) != 3)
                return -EIO;        /*  優先級不爲3,出錯  */
            value &= 0xffff;        /*  ds es爲16位 */
            break;
        case SS:
        case CS:
            if ((value & 3) != 3)
                return -EIO;        /*  優先級不爲3,出錯  */
            value &= 0xffff;        /*  ss cs爲16位 */
            break;
        case EFL:
            value &= FLAG_MASK; /*  EFLAG訪問權限設定  */
            value |= get_stack_long(child, EFL_OFFSET) & ~FLAG_MASK;
    }
    if (regno > GS*4)
        regno -= 2*4;               /*  修正偏移量  */
    put_stack_long(child, regno - sizeof(struct pt_regs), value);
    return 0;
}

static unsigned long getreg(struct task_struct *child,
    unsigned long regno)
{
    unsigned long retval = ~0UL;
    switch (regno >> 2) {
        case FS:
            retval = child->tss.fs;     /*  通過TSS讀寄存器gs  */
            break;
        case GS:
            retval = child->tss.gs;     /*  通過TSS讀寄存器fs  */
            break;
        case DS:
        case ES:
        case SS:
        case CS:
            retval = 0xffff;                /*  ds es ss cs爲16位 */
        default:
            if (regno > GS*4)           /*  修正偏移量  */
                regno -= 2*4;
            regno = regno - sizeof(struct pt_regs);
            retval &= get_stack_long(child, regno);
    }
    return retval;

3.內存讀寫輔助函數

在sys_ptrace函數中對用戶空間的訪問通過輔助函數write_long()和read_long()函數完成的。訪問進程空間的內存是通過調用Linux的分頁管理機制完成的。從要訪問進程的task結構中讀出對進程內存的描述mm結構,並依次按頁目錄、中間頁目錄、頁表的順序查找到物理頁,並進行讀寫操作。函數put_long()和get_long()完成的是對一個頁內數據的讀寫操作。而write_long()和read_long()函數考慮了,所要訪問的數據在兩頁之間,這時則需對兩頁分別調用put_long()和get_long()函數完成其功能。
其中相關函數和流程如下:
這裏寫圖片描述

static unsigned long get_long(struct task_struct * tsk,
    struct vm_area_struct * vma, unsigned long addr)
{
    pgd_t * pgdir;
    pmd_t * pgmiddle;
    pte_t * pgtable;
    unsigned long page;

repeat:
    pgdir = pgd_offset(vma->vm_mm, addr);       /*  查找頁目錄  */
    if (pgd_none(*pgdir)) {
        handle_mm_fault(tsk, vma, addr, 0);     /*  缺頁處理  */
        goto repeat;
    }
    if (pgd_bad(*pgdir)) {
        printk("ptrace: bad page directory %08lx\n", pgd_val(*pgdir));
        pgd_clear(pgdir);                       /*  頁出錯  */
        return 0;
    }
    pgmiddle = pmd_offset(pgdir, addr);         /*  查找中間頁目錄  */
    if (pmd_none(*pgmiddle)) {
        handle_mm_fault(tsk, vma, addr, 0);     /*  缺頁處理  */
        goto repeat;
    }
    if (pmd_bad(*pgmiddle)) {
        printk("ptrace: bad page middle %08lx\n", pmd_val(*pgmiddle));
        pmd_clear(pgmiddle);                    /*  頁出錯  */
        return 0;
    }
    pgtable = pte_offset(pgmiddle, addr);           /*  查找頁表  */
    if (!pte_present(*pgtable)) {
        handle_mm_fault(tsk, vma, addr, 0);     /*  缺頁處理  */
        goto repeat;
    }
    page = pte_page(*pgtable);
    if (MAP_NR(page) >= max_mapnr)
        return 0;                               /*  越界出錯  */
    page += addr & ~PAGE_MASK;
    return *(unsigned long *) page;
}

這裏寫圖片描述

static void put_long(struct task_struct * tsk, struct vm_area_struct * vma, unsigned long addr,
    unsigned long data)
{
    pgd_t *pgdir;
    pmd_t *pgmiddle;
    pte_t *pgtable;
    unsigned long page;

repeat:
    pgdir = pgd_offset(vma->vm_mm, addr);       /*  查找頁目錄  */
    if (!pgd_present(*pgdir)) {
        handle_mm_fault(tsk, vma, addr, 1);     /*  缺頁處理  */
        goto repeat;
    }
    if (pgd_bad(*pgdir)) {
        printk("ptrace: bad page directory %08lx\n", pgd_val(*pgdir));
        pgd_clear(pgdir);                       /*  頁出錯  */
        return;
    }
    pgmiddle = pmd_offset(pgdir, addr);         /*  查找中間頁目錄  */
    if (pmd_none(*pgmiddle)) {
        handle_mm_fault(tsk, vma, addr, 1);     /*  缺頁處理  */
        goto repeat;
    }
    if (pmd_bad(*pgmiddle)) {
        printk("ptrace: bad page middle %08lx\n", pmd_val(*pgmiddle));
        pmd_clear(pgmiddle);                    /*  頁出錯  */
        return;
    }
    pgtable = pte_offset(pgmiddle, addr);           /*  查找頁表  */
    if (!pte_present(*pgtable)) {
        handle_mm_fault(tsk, vma, addr, 1);
        goto repeat;
    }
    page = pte_page(*pgtable);                  /*  讀頁  */
    if (!pte_write(*pgtable)) {                 /*  是否可寫  */
        handle_mm_fault(tsk, vma, addr, 1);     /*  頁出錯  */
        goto repeat;
    }
    if (MAP_NR(page) < max_mapnr)
        (unsigned long ) (page + (addr & ~PAGE_MASK)) = data;  /*  寫數據  */
    set_pte(pgtable, pte_mkdirty(mk_pte(page, vma->vm_page_prot)));
    flush_tlb();
}

這裏寫圖片描述

static int read_long(struct task_struct * tsk, unsigned long addr,
    unsigned long * result)
{
    struct vm_area_struct * vma = find_extend_vma(tsk, addr);  /*  查找對應的VMA  */
    if (!vma)
        return -EIO;                                    /*  出錯  */
    if ((addr & ~PAGE_MASK) > PAGE_SIZE-sizeof(long)) {  /*  是否跨頁訪問  */
        unsigned long low,high;
        struct vm_area_struct * vma_high = vma;
        if (addr + sizeof(long) >= vma->vm_end) {           /*  是否跨VMA訪問  */
            vma_high = vma->vm_next;                    /*  獲得下一個VMA  */
            if (!vma_high || vma_high->vm_start != vma->vm_end)
                return -EIO;                            /*  出錯  */
        }
        low = get_long(tsk, vma, addr & ~(sizeof(long)-1));    /*  低字節  */
        high = get_long(tsk, vma_high, (addr+sizeof(long)) & ~(sizeof(long)-1));
                                                    /*  高字節  */
        switch (addr & (sizeof(long)-1)) {      /*  重新組裝數據  */
            case 1:
                low >>= 8;
                low |= high << 24;
                break;
            case 2:
                low >>= 16;
                low |= high << 16;
                break;
            case 3:
                low >>= 24;
                low |= high << 8;
                break;
        }
        *result = low;
    } else
        *result = get_long(tsk, vma, addr);         /*  非跨頁訪問  */
    return 0;
}

這裏寫圖片描述

tatic int write_long(struct task_struct * tsk, unsigned long addr,
    unsigned long data)
{
    struct vm_area_struct * vma = find_extend_vma(tsk, addr);   /*  查找對應的VMA  */
    if (!vma)
        return -EIO;                                    /*  出錯  */
    if ((addr & ~PAGE_MASK) > PAGE_SIZE-sizeof(long)) {
        unsigned long low,high;
        struct vm_area_struct * vma_high = vma;
        if (addr + sizeof(long) >= vma->vm_end) {           /*  是否跨VMA訪問  */
            vma_high = vma->vm_next;                    /*  獲得下一個VMA  */
            if (!vma_high || vma_high->vm_start != vma->vm_end)
                return -EIO;                            /*  出錯  */
        }
        low = get_long(tsk, vma, addr & ~(sizeof(long)-1));    /*  獲得原來低字節數據  */
        high = get_long(tsk, vma_high, (addr+sizeof(long)) & ~(sizeof(long)-1));
                                                    /*  獲得原來高字節數據  */
        switch (addr & (sizeof(long)-1)) {              /*  重新組裝要回寫的數據  */
            case 0: 
                low = data;
                break;
            case 1:
                low &= 0x000000ff;
                low |= data << 8;
                high &= ~0xff;
                high |= data >> 24;
                break;
            case 2:
                low &= 0x0000ffff;
                low |= data << 16;
                high &= ~0xffff;
                high |= data >> 16;
                break;
            case 3:
                low &= 0x00ffffff;
                low |= data << 24;
                high &= ~0xffffff;
                high |= data >> 8;
                break;
        }
        put_long(tsk, vma, addr & ~(sizeof(long)-1),low);      /*  寫低字節數據  */
        put_long(tsk, vma_high, (addr+sizeof(long)) & ~(sizeof(long)-1),high);
                                                    /*  寫高字節數據  */
    } else
        put_long(tsk, vma, addr, data);     /*  非跨頁訪問  */
    return 0;
}

4.信號處理

在進程接受到信號時,如果判斷進程被跟蹤,則中止當前進程,並通知其父進程。其操作在函數do_signal中處理。其處理流程如下:
這裏寫圖片描述
其代碼和說明註釋如下:

if ((current->flags & PF_PTRACED) && signr != SIGKILL) {
                            /*  判斷是否被跟蹤  */
            current->exit_code = signr;     /*  通知父進程造成中止的信號  */
                                        /*  父進程wait接收  */
            current->state = TASK_STOPPED;/*  進程狀態置爲TASK_STOPPED  */
            notify_parent(current, SIGCHLD);    /*  通知父進程SIGCHLD    */
            schedule();                 /*  重新調度,使調試器運行  */

            /*  調試器返回,繼續執行  */
            if (!(signr = current->exit_code))  /*  是否忽略造成中止的信號  */
                continue;
            current->exit_code = 0;
            if (signr == SIGSTOP)           /*  忽略SIGSTOP信號
                continue;
            if (signr != info.si_signo) {
                info.si_signo = signr;              /*  更新siginfo結構  */
                info.si_errno = 0;
                info.si_code = SI_USER;
                info.si_pid = current->p_pptr->pid;
                info.si_uid = current->p_pptr->uid;
            }
        if (sigismember(&current->blocked, signr)) { /*  信號是否被阻塞  */
                send_sig_info(signr, &info, current);    /*  把信號加入隊列  */
                continue;
            }
        }

5.系統調用跟蹤

系統調用跟蹤是一種使被調試進程在進入系統調用或完成系統調用時,中止進程的調試方法。此功能相當於在進入系統調用或系統調用完處設置斷點。調試器通過調用ptrace(PTRACE_SYSCALL)使進程繼續運行,直到系統調用開始或結束。在ptrace(PTRACE_SYSCALL)處理中,設置了進程標誌PF_TRACESYS。在系統調用時如果判斷進程標誌設置了PF_TRACESYS則調用函數syscall_trace。代碼如下:

(/linux/arch/i386/kernel/entry.S)
    testb $0x20,flags(%ebx)        # PF_TRACESYS /*  判斷PF_TRACESYS標誌  */
    jne tracesys
    …….
tracesys:
    movl $-ENOSYS,EAX(%esp)
    call SYMBOL_NAME(syscall_trace)         /*  調用syscall_trace  */
    movl ORIG_EAX(%esp),%eax
    call *SYMBOL_NAME(sys_call_table)(,%eax,4)  /*  調用系統調用  */
    movl %eax,EAX(%esp)     # save the return value
    call SYMBOL_NAME(syscall_trace)         /*  調用syscall_trace  */
    jmp ret_from_sys_call

syscall_trace函數在/linux/arch/i386/ptrace.c中定義。syscall_trace函數完成了系統調用中斷的功能,其流程如下:
這裏寫圖片描述
程序及說明註釋如下:

asmlinkage void syscall_trace(void)
{
    if ((current->flags & (PF_PTRACED|PF_TRACESYS)) 
            != (PF_PTRACED|PF_TRACESYS))    /*  判斷是否系統調用跟蹤  */
        return;
    current->exit_code = SIGTRAP;           /*  通知父進程中止原因SIGTRAP  */
    current->state = TASK_STOPPED;      /*  進程狀態設置爲TASK_STOPPED  */
    notify_parent(current, SIGCHLD);            /*  通知父進程SIGCHLD  */
    schedule();
    /*  重新調度,執行調試器  */
    /*  ……                   */
    /*  調試器命令繼續執行    */
    if (current->exit_code) {                       /*  是否忽略信號  */
        send_sig(current->exit_code, current, 1);   /*  繼續信號  */
        current->exit_code = 0;
    }
}

6.調試陷阱處理

調試異常的編號爲1,在Linux(i386)中由\linux\arch\i386\kernel\traps.c中的函數do_debug完成。在i386中引起調試異常的條件有:程序斷點(指令和數據斷點)、單步執行、TSS調試陷阱。do_debug函數處理中對於正常的調試異常產生SIGTRAP信號,正常調試異常指通過ptrace調試進程,進程在調試狀態下。對於一些非正常的調試異常則做一些清理工作,非正常的調試有可能是用戶進程故意引起或一些寄存器沒有初始化或設置。
do_debug的處理流程如下:
這裏寫圖片描述
程序及說明註釋如下:

asmlinkage void do_debug(struct pt_regs * regs, long error_code)
{
    unsigned int condition;         /*  DR6調試狀態  */
    struct task_struct *tsk = current;

    if (regs->eflags & VM_MASK)         /*  判斷是否是虛擬8086方式  */
        goto debug_vm86;

    __asm__ __volatile__("movl %%db6,%0" : "=r" (condition));   /*  讀取DR6值  */

    if (condition & DR_STEP) {              /*  是否爲單步異常  */
        if ((tsk->flags & (PF_DTRACE|PF_PTRACED)) == PF_DTRACE)  
    /*  是否設置PF_PTRACED  */
            goto clear_TF;                  /*  爲了防止用戶態進程修改TF標誌  */
                                        /*  以及TF標誌錯誤設置造成異常    */
    }

    if (condition & (DR_TRAP0|DR_TRAP1|DR_TRAP2|DR_TRAP3)) {
                                        /*  是否爲斷點異常  */
        if (!tsk->tss.debugreg[7])              /*  是否TSS中DR7爲0  */
            goto clear_dr7;             /*  爲了防止DR7的錯誤設置導致異常  */
    }

    if ((regs->xcs & 3) == 0)                   /*  是否爲核心引起異常  */
        goto clear_dr7;

    tsk->tss.trap_no = 1;
    tsk->tss.error_code = error_code;
    force_sig(SIGTRAP, tsk);                /*  產生SIGTRAP信號  */
    return;

debug_vm86:                             /*  轉向虛擬8086陷阱處理  */
    lock_kernel();
    handle_vm86_trap((struct kernel_vm86_regs *) regs, error_code, 1);
    unlock_kernel();
    return;

clear_dr7:                              /*  清除DR7  */
    __asm__("movl %0,%%db7"
        : /* no output */
        : "r" (0));
    return;

clear_TF:                                   /*  清除TF標誌  */
    regs->eflags &= ~TF_MASK;
    return;
}

7.execve系統調用

execve系統調用完成的功能是,把當前進程代碼替換爲新的二進制代碼,並執行。調用execve系統調用的進程如果已經被調試(PF_PTRACED置位),則把代碼轉入後則會向自身發送信號SIGTRAP,使其中止執行。這樣可以使調試器裝入代碼後,進程停止在代碼的最開始。
對於不同的二進制文件,execve調用不同的代碼。以下是aout和elf文件格式的代碼轉入程序中完成上述功能的代碼。

do_load_elf_binary()函數(/linux/fs/binfmt_elf.c)
    。。。
    if (current->flags & PF_PTRACED)
        send_sig(SIGTRAP, current, 0);
    。。。
do_load_aout_binary()函數(/linux/fs/binfmt_aout.c)
    。。。
    if (current->flags & PF_PTRACED)
        send_sig(SIGTRAP, current, 0);
    。。。

四、ptrace的使用

ptrace 提供了一種父進程可以控制子進程運行,並可以檢查和改變它的核心的功能,ptrace大多被調試器所使用。

1.啓動、中止調試程序

1)啓動調試程序

調試器對一個程序進行調試,如果此程序沒有在運行,則調試器需要裝入其代碼,並創建相應的進程,並使其進入調試狀態。其使用ptrace實現的方法如下:

int pid;
  pid = fork ();
  if (pid < 0)
    perror_with_name ("fork");
  if (pid == 0)
    {
      ptrace (PTRACE_TRACEME, 0, 0, 0);
      execv (program, allargs);    /* char *program;
                                char **allargs; 指向程序名和參數
      fprintf (stderr, "Cannot exec %s: %s.\n", program,
           errno < sys_nerr ? sys_errlist[errno] : "unknown error");
      fflush (stderr);
      _exit (0177);
}
    wait(pid);
ptrace (PTRACE_CONT, pid, 0, 0);
wait(pid);

首先調試器需創建進程、裝入代碼。調用fork(),創建子進程。對於子進程首先調用ptrace (PTRACE_TRACEME, 0, 0, 0),進程標誌PF_PTRACED置位,子進程進入調試狀態。再執行execv系統調用,系統裝入可執行文件代碼,裝入完畢後,則會向自身發送SIGTRAP信號(參見execve分析)。這時,子進程已經進入調試狀態(PF_PTRACED置位),則在處理SIGTRAP信號的時候, 中止進程執行,並通知調試器(其父進程)讓其運行,父進程則從wait調用中返回(參見do_signal分析)。至此,要調試的程序已經調入、並作爲調試器的一個子進程運行,並中止在程序的第一條指令上。

接着爲了讓子進程運行,父進程調用ptrace (PTRACE_CONT, pid, 0, 0),從而使子進程繼續執行。再調用wait等待子進程中斷或退出。
在GDB調試器中命令run則是通過上述方法完成運行調試運行程序的。

2)對現有進程進行調試

調試器還能對已經運行的進程進行調試。通過調用ptrace的PTRACE_ATTACH功能可以實現。具體做法如下:

ptrace(PTRACE_ATTACH,pid, 0,0);
wait(pid);

首先調用ptrace(PTRACE_ATTACH,pid, 0,0)。ptrace爲完成對這個進程的調試設置,首先設置進程標誌置PF_PTRACED。再將它設置爲調試器的子進程,最後向它發信號SIGSTOP中止它的運行,使它進入調試狀態。(具體分析見ptrace函數分析)
調試器在調用ptrace後需調用wait,等待要調試的進程進入STOP狀態。
在GDB調試器中命令attach則是通過上述方法調試現有的進程的。

3)退出對進程調試

對於使用ptrace的PTRACE_ATTACH功能調試的程序,希望放棄調試,使其繼續執行時可以使用ptrace的PTRACE_DETACH功能。

調用了ptrace(PTRACE_DETACH,pid, 0,0),終止調試一個子進程。此處理與PTRACE_ATTACH處理相反。在此做了一些清理操作:清除PF_TRACESYS和PF_PTRACED進程標誌,清除TF標誌,父進程指針還原。最後喚醒此進程,讓其繼續執行(具體分析見ptrace函數分析)。

在GDB調試器中命令detach則是通過上述方法調試現有的進程的。

4)終止調試進程運行

對被調試的進程,不想再調試時,可以調用ptrace的PTRACE_KILL功能殺死被調試的進程。

調用了ptrace(PTRACE_KILL,pid, 0,0),把子進程繼續的信號設置爲SIGKILL,然後喚醒子進程,由於子進程是在do_signal處理中進入stop的,所以它將繼續處理SIGKILL部分的代碼,從而使子進程終止。(具體分析見ptrace函數分析)

在GDB調試器中命令kill則是通過上述方法調試現有的進程的。
在使用調試器調試程序時,被調試程序被中斷的條件有::
1.調試器設置的斷點(指令斷點和數據斷點)滿足條件。
2.進程收到一個信號(SIGKILL除外)。
3.單步調用完成。
4.系統調用調試下,進入或離開系統調用。

A.斷點

設置斷點是調試器中的一個重要功能。80386提供了兩種方式,INT3和利用調試寄存器(詳見前面80386的調試設施)。

如果使用INT3方式設置斷點,則調試器通過ptrace的PTRACE_POKETEXT功能在斷點處插入INT3單字節指令。當進程運行到斷點時(INT3處),則系統進入異常3的處理。
若使用調試寄存器,則調試器通過調用ptrace(PTRACE_POKEUSR,pid,0,data)在DR0-DR3寄存器設置與四個斷點條件的每一個相聯繫的線性地址在DR7中設置斷點條件。被跟蹤進程運行到斷點處時,CPU產生異常 1,從而轉至函數do_debug處理。由於子進程在調試狀態下屬於正常調試異常,所以do_debug函數處理中產生SIGTRAP信號,爲處理這個信號,進入do_signal,使被調試進程停止,並通知調試器(父進程),此時得到子進程終止原因爲SIGTRAP。

B.信號
在有些情況之下,要求調試器調試某進程時,當進程收到某一信號的時候中斷進程運行。如:被調試進程在某處運算錯誤,進程會接收到SIGFPE信號,在正常運行狀況下,會Coredump,而調試的情況下則希望在產生錯誤代碼處停止運行,可以讓用戶調試錯誤原因。

對於已經被調試的進程(PF_PTRACED標誌置位),當受到任何信號(SIGKILL除外)會中止其運行,並通知調試器(父進程)。(詳見do_signal分析)

C.單步執行
單步執行也是一種使進程中止的情況。當用戶調用ptrace的PTRACE_SINGLESTEP功能時,ptrace處理中,將用戶態標誌寄存器EFLAG中TF標誌爲置位,並讓進程繼續運行(具體分析見ptrace函數分析)。當進程回到用戶態運行了一條指令後,CPU產生異常 1,從而轉至函數do_debug處理。由於子進程在調試狀態下屬於正常調試異常,所以do_debug函數處理中產生SIGTRAP信號,爲處理這個信號,進入do_signal,使被調試進程停止,並通知調試器(父進程),此時得到子進程終止原因爲SIGTRAP。

D.系統調用調試
對程序的調試,有時希望對系統調用進程跟蹤。當程序進行系統調用時中斷其運行。ptrace提供PTRACE_SYSCALL功能完成此功能。在ptrace調用中設置了進程標誌PF_TRACESYS,表示進程對系統調用進行跟蹤,並繼續執行進程(具體分析見ptrace函數分析)。直到進程調用系統調用時,則中止其運行,並通知調試器(父進程)。(詳見syscall_trace分析)

2.繼續進程執行

讓中斷的進程繼續執行,ptrace提供三種功能

1.繼續執行(PTRACE_CONT)
2.系統調用調試(PTRACE_SYSCALL)
3.單步執行(PTRACE_SINGLESTEP)

三種功能的區別在於PTRACE_CONT功能讓進程繼續執行直到下一個斷點或收到一個信號會中止進程運行。PTRACE_SYSCALL功能讓進程繼續執行增加了一箇中止條件,進程調用系統調用。PTRACE_SINGLESTEP功能讓進程繼續執行,只執行一個機器指令,則就中止其運行。

當被調試進程因爲受到一個信號而中止時,這個信號並沒有被處理。如果希望繼續運行進程時繼續處理這個信號,則在上述三個ptrace功能調用時,最後一個參數data設置要繼續處理的信號。這種情況出現在,如:中止進程運行的信號爲用戶自定義信號,用戶想繼續運行進程,而不要忽略用戶信號處理。有時,用戶希望忽略其信號處理,這時則參數data設置爲0,這種情況出現在,如:由於算術錯誤接收到SIGFPE信號使進程中止,而用戶發現了錯誤,重新設置了正確的值,然後希望其繼續執行,這時SIGFPE信號則需要忽略。

3.進程數據存取

當被調試進程中止的狀態下,調試器一般提供給用戶觀察、修改程序變量的功能,以及反彙編代碼,以及觀察、修改內存地址和寄存器的功能。讀寫代碼段數據使用ptrace的PTRACE_PEEKTEXT和PTRACE_POKETEXT功能。讀寫數據段數據使用ptrace的PTRACE_PEEKDATA和PTRACE_POKEDATA功能。讀寫寄存器數據使用ptrace的PTRACE_PEEKUSR和PTRACE_POKEUSR功能。對於Linux(i386)下可以使用ptrace的PTRACE_GETREGS和PTRACE_PUTREGS讀寫所有i386用戶寄存器,使用ptrace的PTRACE_GETFPREGS和PTRACE_PUTFPREGS讀寫所有i387浮點寄存器。

發佈了45 篇原創文章 · 獲贊 31 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章