如何在用戶態完成一次上下文切換

X64棧幁結構

  • X64整體的棧幁整體以下圖
    x64棧幁結構git

  • GCC沒有優化的狀況下的反編譯
    x64反編譯github

棧幁模型詳述

在線反彙編地址app

  • 讀者經過上圖觀察能夠看到函數的第一個參數放在RDI寄存器,第二個參數放在RSI寄存器 ... 後續參數放在何處,相關調用約定能夠查看AMD 64 調用約定jvm

  • 讀者從上圖能夠看出 ,在彙編層面上,由函數的被調用方保存 rsp rbp 指針, 函數a b c 在彙編層面都是首先將 rbp入棧(push rbp) 而後對rsp操做 (例如sub rsp , 16;)並都會在函數返回前分別 將rbp出棧 (pop rbp) 還原rsp (例如 add rsp, 16;)函數

  • 另外X86864 fastcall約定使用RAX寄存器保存返回值oop

總結一下彙編的函數的模板大體以下優化

  1. 保護當前幀的調用方的rbp寄存器

保護調用方的rbp

  1. 將當前的rsp指針做爲當前幀的rbp指針,防止破壞當前調用方幀的臨時變量

指定當前函數的rbp

  1. 將rsp指針減去固定的數量 並存回rsp寄存器, 防止接下來當前幀中的被調用方破壞當前幀中的臨時變量

防止破壞當前幀中的數據

  1. 返回前將當前幀須要返回的結果寫入RAX寄存器,這一步比較隱晦 由於只有函數c 出現了這個步驟,在實際中函數返回都會依照約定寫入RAX寄存器,因爲函數b自己並無對函數返回結果(RAX寄存器的值)進行加工,所以函數b返回的時候RAX寄存器的值並不變化,同理a也是如此。ui

  2. 分別恢復rsp rbp寄存器 ,以下圖中 add rsp, 16 跟 pop rbp操作系統

分別恢復rsp rbp寄存器

經過上面幾個套路模板,基本上咱們就能使用匯編來編寫彙編函數了,而後記住要很是當心地操做寄存器,避免破壞調用方的棧幁(函數中的臨時變量)。線程

如何在用戶態完成一次上下文切換

  • X64下一個線程在運行的時候,有一個PC寄存器指向當前線程的彙編代碼的位置,咱們須要經過更換PC寄存器中的值 讓CPU接下來從PC寄存器中新的位置運行彙編代碼

  • 另外咱們但願 從當前的上下文切換到另一個上下文後,CPU可以切換回來繼續正常運行,根據前面所說,那麼咱們須要保護當前上下文的 RSP RBP 指針,而且保證當前上下文的整個棧幁的區域不會被另一個上下文給破壞

切換的具體步驟

參考 我以前寫的 JVM暫停工做線程機制

  1. 註冊信號,讓操做系統 接下來切換上下文到 handler函數
//全局變量
char *buffer;
int pagesize;

void allocate_memory() {

    /*
     * 初始化信號量結構體
     */
    struct sigaction sa;

    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    // 註冊 handler 函數 ,當咱們觸發信號的時候 操做系統會將切換到 handler 這個函數上下文來運行代碼
    sa.sa_sigaction = handler;
    if (sigaction(SIGSEGV, &sa, NULL) == -1)
        handle_error("sigaction");

    pagesize = sysconf(_SC_PAGE_SIZE);
    if (pagesize == -1)
        handle_error("sysconf");

    // 初始化buffer指針指向的內存區域
    buffer = memalign(pagesize, 4 * pagesize);
    if (buffer == NULL)
        handle_error("memalign");

    printf("Start of region:        0x%lx\n", (long) buffer);

    //設置buffer指針爲只讀,接下來若是訪問到buffer指針指向的內存區域就會觸發信號
    if (mprotect(buffer, pagesize * 4,
                 PROT_READ) == -1)
        handle_error("mprotect");
}
  1. 觸發操做系統信號前 作一些準備工做
int main(int argc, char *argv[]) {
    /*
     * allocate memory and set memory access READ
     */
    allocate_memory();
    char *p = buffer;

    // 初始化當前函數調用幀中的3個臨時變量
    uint64 local_pc = 0;
    uint64 local_rsp = 0;
    uint64 local_rbp = 0;

    //如下彙編代碼是我根據C語言反編譯後肯定 三個local_變量的位置
    __asm__(".intel_syntax;"
            // 將當前CPU運行的代碼的位置寫入 RAX寄存器 方便handler中切換回來
           // 其實這個rax寄存器中最終存放的內存地址 也就是 lea rax, [rip] 這個彙編代碼在內存中的位置。
            "lea rax, [rip];"
            //保存RAX寄存器 到 local_pc變量
            "mov [rbp-0x20], rax;"
            //保存RSP寄存器 到 local_rsp變量
            "mov [rbp-0x28], rsp;"
            //保存RBP寄存器 到 local_rbp變量
            "mov [rbp-0x30], rbp;"
    );

    //寫入全局變量
    pc = local_pc;
    rsp = local_rsp;
    rbp = local_rbp;


    for (int i = 0; i < 4; i++) {
       //當咱們操做p指針也就是buffer指針的時候,就會觸發信號
        *(p) = 'a';
        p++;
    }
    *(p) = '\0';
    printf("p = %s\n", buffer);
    //printf("%x",local_pc);


    /*
     * if we didn't restore those registers,
     * it should not happen.
     */
    printf("Loop completed\n");
    exit(EXIT_SUCCESS);
}
  1. 信號觸發後
static void handler(int sig, siginfo_t *si, void *unused) {
    /*
     * 打印受保護的內存地址
     */
    printf("Got SIGSEGV at address: 0x%lx\n",
           (long) si->si_addr);

    // 取消buffer指針指向內存區域的內存保護權限
    if (mprotect(buffer, pagesize * 4,
                PROT_READ | PROT_WRITE) == -1)
        handle_error("mprotect");

    // 此時線程的控制權 已經歸JVM代碼掌控,JVM能夠掛起當前線程,等完成GC垃圾回收工做後再恢復狀態

    // 老辦法 將以前全局變量保存的寄存器值恢復到當前函數幀中的臨時變量
    uint64 local_rsp = rsp;
    uint64 local_pc = pc;
    uint64 local_rbp = rbp;

    
    // 具體三個 local_xxx 變量的地址,依舊是我經過反編譯C程序肯定的
   // 仍是老辦法 該怎麼寫到全局變量的 又怎麼經過本地變量 local_xxx 寫回到寄存器裏面去
    __asm__(".intel_syntax;"
            "mov rsp,[rbp-0x20];"
            "mov rax,[rbp-0x28];"
            "mov rbp,[rbp-0x30];"
            // 此處很是關鍵 直接jmp 調回去 讓CPU回到以前main函數的上下文繼續執行代碼
            "jmp rax;"
    );

    //never happen
    printf("rsp:%x", local_rsp);
    printf("pc:%x", local_pc);
    printf("rbp:%x", local_rbp);
    exit(EXIT_FAILURE);
}

整個流程的DEBUG的GIF以下

debug運行的流程

完整的代碼

聯繫我

  • 若是有不懂的地方,歡迎經過留言聯繫我
相關文章
相關標籤/搜索