Linux:ブレークポイントの原理と実現


前言
プログラミングの仕事をしている私たちは、IDEで開発中のコードをデバッグしても、GDBで実行中のプロセスをチェックしても、デバッグのタイミングがあります.
特にGDBをよく使う子供靴は、breakpoint(ブレークポイント)のような強力な機能を提供しています.
ちょうど最近Ptraceに関する実験ができたので、ついでにこの小文を書いてブレークポイントの道理を分かち合いました.
単純GDBモデル
// test.cpp

#include
#include

void test1(){
    std::cout << "test" << std::endl;
}

int main() {
    while (true) {
        std::cout << "main: " << getpid() << std::endl;
        test1();
        sleep(1);
    }
    return 0;
}

コンパイル運転
g++ -std=c++11 test.cpp && ./a.out

//   
main: 22346
test
main: 22346
test
main: 22346
...

GDBを開き、test 1関数のブレークポイント
sudo gdb a.out -p 22346

//   
... (       ,       )

(gdb) break test1       //   test1     
Breakpoint 1 at 0x40091a

(gdb) c                 //     
Continuing.

Breakpoint 1, 0x000000000040091a in test1() ()

(gdb) i r rip           //    cpu         
rip            0x40091a 0x40091a 

a.outの出力を振り返るとmain:5693に止まって印刷されず、プロセス状態もT:
T状態とは、(TASK_STOPPED or TASK_TRACED)、休止状態またはトレース状態を意味し、その後GDBにより各種デバッグの動作が可能となる.
今回も同様の効果を実現しますが、指定された位置で一時停止し、プロセスの制御権を得ることだけを考慮した超簡略化バージョンです.
事前知識の準備
実現する前に、必要な知識を理解する必要があります.
レジスタ:RIP
レジスタの子供靴を知らなかったら、https://www.jianshu.com/p/029...を見てみましょう.
直接中の説明を抜粋します.
rip        ,     CPU           。

   CPU             ,rip            ;

Ptrace
前にPtraceの子供靴を知らなかったら、まず見てみてください:http://fancy-blogs.com/2018/0...
ptraceには2つのロールがあります.
  • tracee:ptraceシステムによって呼び出された操作がその上に作用する監視されたプロセスである追跡者(例えば、上記の22346プロセス).
  • tracer:追跡者、それは追跡者から伝達された情報(例えば:GDB)を監視し処理する責任を負う.

  • 以下、この2つの名詞を直接引用します.
    実現構想.
    実現の構想は非常に簡単である
    1.ブレークポイントのアドレスを確認する
    GDBでは,行番号や関数名に直接ブレークポイントを設定する習慣があり,行番号は相対的に複雑であり,まず関数名を示す.
    Linux環境でコンパイルされた実行可能ファイルはすべてELF形式に従っており、特別な処理がなければ、比較的完全なシンボルテーブルが保持されます.
    冒頭のプログラムを例にとると、readef-s a.outで表示できます.
    このシンボルテーブルには、プロセスで使用するシンボルがそれぞれどの位置にあるかが記録されています.
    図のように、最初の列は記号のアドレス(16進法)、2番目の列は長さ、最後の列は記号の名前です.
    ここではtest 1という関数のブレークポイント、つまり赤い輪が出ているところに子供靴があるかもしれません.なぜですか.Z5test1v
    ここでは主にcppの名前修飾問題:https://blog.csdn.net/u013220...、邪魔ではありません.
    前のアドレスが0 x 400916であることがわかります.
    2.Ptraceによりtraceeの制御権を得る
     //        ,          PTRACE_ATTACH,   PTRACE_SEIZE      ,       tracee,  PTRACE_SEIZE   
    ptrace(PTRACE_SEIZE, pid, addr, data) 
    
    //    tracee    ,       tracer
    ptrace(PTRACE_INTERRUPT, pid, addr, data)       
    
    //    tracee      ,       
    waitpid(pid, &status, options)

    3.現在のripの命令内容を保持し、割り込み命令で置き換える
    //    tracee addr      
    ptrace(PTRACE_PEEKDATA, pid, addr, data)  
    
    //    tracee        
    ptrace(PTRACE_POKEDATA, pid, addr, data) 
    
    //    tracee         
    ptrace(PTRACE_GETREGS, pid, addr, data) 
      
    //    tracee         
    ptrace(PTRACE_SETREGS, pid, addr, data)   

    4.運転を再開し、trapトリガを待つ
    //   tracee       
    ptrace(PTRACE_CONT, pid, addr, data)  

    5.ripコマンドを復元し、デバッグを終了する
    完全トレースコード
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void dowait(pid_t pid) {
        int status, signum;
        while (true) {
            waitpid(pid, &status, 0);
            if (WIFSTOPPED(status)) {
                signum = WSTOPSIG(status);
                if (signum == SIGTRAP) {
                    break;
                } else {
                    std::cout << "Other signum, skipping..." << std::endl;
                    ptrace(PTRACE_CONT, pid, 0, 0);
                }
            }
        }
    }
    
    
    void break_onece(pid_t pid, long addr) {
    
        //    addr         (    rip)
        long old_code = ptrace(PTRACE_PEEKDATA, pid, addr, NULL);
        user_regs_struct old_regs;
        ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);
    
    
        long trap_code = old_code;
        unsigned char *p = (unsigned char*) &trap_code;
    
        // Trap            
        p[0] = 0xcc;
    
        //   Trap    addr   ,  cpu         
        if (ptrace(PTRACE_POKEDATA, pid, addr, trap_code)) {
            std::cout << "Break failed" << std::endl;
            return;
        }
    
        ptrace(PTRACE_CONT, pid, NULL, NULL);
        dowait(pid);
    
        //          ,            (     !!!)
        std::cout << "Next ? " << std::endl;
        std::string instruction;
        std::cin >> instruction;
    
        //    rip,          rip    tracee coredump
        ptrace(PTRACE_SETREGS, pid, NULL, &old_regs);
    
        //    addr   
        ptrace(PTRACE_POKEDATA, pid, addr, old_code);
        ptrace(PTRACE_CONT, pid, 0, 0);
    }
    
    void quit(pid_t pid) {
        ptrace(PTRACE_DETACH, pid, NULL, NULL);
        std::cout << "quit!" << std::endl;
        exit(0);
    }
    
    int main(int argc, char* argv[]) {
        pid_t pid = std::stoi(argv[1]);
    
        if (ptrace(PTRACE_SEIZE, pid, NULL, NULL)) {
            perror("ptrace_seize failed");
            return -1;
        }
    
        if(ptrace(PTRACE_INTERRUPT, pid, 0, 0)) {
            perror("interrupt failed");
            quit(pid);
        }
    
        dowait(pid);
    
        //       
        long break_addr = 0x400916;
        break_onece(pid, break_addr);
    
        quit(pid);
        return 1;
    }

    コンパイル&実行
    g++ trace_test.cpp -std=c++11 -o trace_test
    
    ./trace_test 22346 #         

    まとめ
    断点の原理についてネット上で多くの文章が言及して、しかし比較的に多くてトンボが水を点けたことがあって、意はまだ尽きないで、いっそ直接最も浅い例でみんなの練習コストを下げます!
    実際には、ここで説明した例にも、最適化できる点がたくさんあります.
  • 例えば、関数アドレス取得の方式は、ELFのシンボルテーブルに言及した以上、このテーブルを解析することによって、ユーザが入力したユーザ名をアドレスに変換すべきである.
  • 例えば、グローバルなブレークポイントテーブルを維持し、任意の多くのブレークポイントを保存し、各ブレークポイントで再利用できるようにしなければならない.
  • はまた、例えば、Ptraceに関連するエラーの戻りは優雅に処理されなければならない.各戻り値が0でない場合、次のステップを簡単に行うことは非常に危険であり、tracee coredumpを招く可能性が高いからである.

  • どれも例えば研究ができるので、後続を期待してください.
    各位の大神の指导の交流を歓迎して、QQ讨论群:258498217転载して出所を明记してください:https://segmentfault.com/a/1190000021870750