1. 程式人生 > >Linux原始碼分析之Ptrace

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