簡體   English   中英

匯編代碼導致遞歸

[英]Assembly code causes recursion

我一直在編寫一個C應用程序,我需要x86匯編語言。 我對匯編很新,下面的代碼片段會導致遞歸:

unsigned int originalBP;
unsigned fAddress;
void f(unsigned short aa) {
    printf("Function %d\n", aa);
}

unsigned short xx = 77;
void redirect() {
    asm {
        pop originalBP
        mov fAddress, offset f
        push word ptr xx
        push fAddress
        push originalBP
    }
}

如果我調用redirect ,它將重復輸出:“功能1135”

首先,以下是有關執行此代碼的環境的一些信息:

  • 編寫此代碼以在NTVDM下執行
  • 使用微小的內存模型(所有段指針寄存器指向同一段)

這是我對上面代碼應該做什么的期望(這很可能是錯誤的罪魁禍首):

  • 彈出堆棧並將值存儲在originalBP ; 我相信該值實際上是當前函數的地址,即redirect
  • f的參數值( xx值)推入堆棧
  • f地址推送到堆棧(因為只有一個段,只需要偏移)
  • 推回redirect的地址

當然,如果這是正確的流程,遞歸將是明顯的(除了打印1135而不是7的部分)。 但有趣的是,對於沒有參數的函數執行相同操作只會產生一行輸出,即:

unsigned int originalBP;
unsigned fAddress;
void f() {
    printf("Function");
}


void redirect() {
    asm {
        pop originalBP
        mov fAddress, offset f
        push fAddress
        push originalBP
    }
}

這可能意味着我對上述代碼的理解是完全錯誤的。 這段代碼中的真正問題是什么?

編輯:我可能留下一些未說明的事情:

  • 這是一個16位應用程序
  • 使用的編譯器是Borland C ++ 3.1,作為Eclipse插件
  • redirectmain調用為redirect()

EDIT(關於Margaret Bloom的回答)這是調用redirect的指令執行示例。 括號中的值表示堆棧指針寄存器以及每個指令執行前該位置的值:

  • 呼叫重定向
  • (FFF4-04E6) push bp
  • (FFF2-FFF6) mov bp, sp
  • (FFF2- mov fAddress, offest fmov fAddress, offest f
  • (FFF2-FFF6) pop originalBP
  • (FFF4-04E6) pop originalRIP
  • (FFF6-0000) push xx (我已將xx更改為1187)
  • (FFF4-0755) push originalRIP
  • (FFF2-04E6) push fAddress
  • (FFF0-04AC) push originalBP
  • (FFEE-FFF6) pop bp
  • (FFF0-04AC) ret
    • (在f中)(FFF2-04E6) push bp
    • (FFF0-FFF6) mov bp,sp
    • printf執行
    • (FFF0-FFF6) pop bp
    • (FFF2-04E6) ret下一個語句好像是return 0; 這是主要的結束。

執行繼續通過一堆行,並以某種方式回到調用redirect的行。

對於你的第二個片段,沒有參數的片段,堆棧狀態如下:

Where                 | Stack (growing on the left)
----------------------+----------------------------
after redirect prolog   redirect rip, redirect bp
pop originalBP          redirect rip
push fAddress           redirect rip, fAddress
push originalBP         redirect rip, fAddress, redirect bp
after redirect epilog   redirect rip, fAddress
after redirect return   redirect rip (control moved to f)
after f prolog          redirect rip, f bp
after f epilog          redirect rip
after f return          (control moved to redirect caller)

其中redirect rip表示函數redirect的返回地址( 返回IP )。

正如你所看到的,在進入的f堆棧正確地指向redirect rip ,返回地址redirect 退出時,控件返回redirect調用者。

對於您的第一個代碼段,堆棧如下:

Where                 | Stack (growing on the left)
----------------------+----------------------------
after redirect prolog   redirect rip, redirect bp
pop originalBP          redirect rip
push word ptr xx        redirect rip, xx
push fAddress           redirect rip, xx, fAddress
push originalBP         redirect rip, xx, fAddress, redirect bp
after redirect epilog   redirect rip, xx, fAddress
after redirect return   redirect rip, xx (control moved to f)
after f prolog          redirect rip, xx, f bp
after f epilog          redirect rip, xx
after f return          (control moved to xx)

進入f redirect rip, xx當我們真正擁有xx, redirect rip時,我們在堆棧上redirect rip, xx
對於前一個配置,參數aa包含redirect的返回地址, f的返回地址是xx的值。


根據您對我的評論回答,代碼意外循環。


如果要使用參數調用f ,請確保在返回地址之前將它們推送:

pop originalBP
pop originalRIP

;Arguments go here    
push xx

push originalRIP
push fAddress
push originalBP

您沒有發布用於編碼redirect編譯器和編譯選項。

在優化ON的情況下,你不能假設將使用完整的C函數prologue / epilogue,所以你使用堆棧操作而不知道它的布局(如果沒有序言/結尾,那么你確實注入了2個值)返回地址給調用者,所以重定向只會返回調用者(主?),這可能基本上只是退出 - >沒有調用f =不是你的情況)。

在asm塊里面你已經有了fn地址,為什么不簡單地調用呢? 堆棧就像:有人調用重定向 - >重定向調用一些地址 - >地址fn() - >返回重定向 - >返回調用者。

它看起來像你試圖修改它:有人調用重定向 - >重定向調用一些地址 - >地址fn() - >返回調用者(跳過返回重定向)。 由於重定向結尾是一小段代碼,我沒有看到修改的好處(我也看不出它是如何與“上下文切換”相關)。

無論如何,檢查你的編譯器選項如何生成最終代碼的匯編列表,看看它是如何真正編譯的,甚至更好,用調試器檢查它(在匯編級別的每個指令步驟)。


編輯(提供調試信息后):

當你到return 0 ,有更多的外國人xx堆棧(注射sp0xFFF4 ),而不是sp是原FFF6指向0

main的結尾可能沒有正確處理這個(我猜是pop bp ret ),假設sp在返回時是正確的。 (它會做其他的C結尾,包括mov sp,bp ,它可能會在你的堆棧篡改中存活下來)。

然后,如果它會在所有函數中執行其他結尾,它也會在redirect() ,因此您還必須修改bp以使redirect()的結尾retfAddress dec bp, dec bp一樣dec bp, dec bp可能就足夠了,因為你通過將2B注入params空間來增加堆棧。

當main中的return 0被擊中時, return 0檢查調試,它是如何實現的,如果它可以應對修改的sp (很明顯它不能,因為它意外地循環到redirect )。

如果是這種情況,你應該修補main來恢復sp然后return 0; 我想知道簡單的mov sp,bp是否會這樣做( bp應該是FFF6之前的那個)。

結論:在多個調用中篡改堆棧幀總是很棘手。 ;)

那么,你是否正朝着這樣的方向前進? (因為我不能完全把手指放在你的問題代碼將被使用的地方,似乎是基本的堆棧練習,讓你了解代碼執行是如何受到影響的,后來可能會發展成這樣的東西..也許......也許不是)。

假冒上下文切換在16b中的某些類似C的偽代碼(好的,更像是評論:)),必須安裝為一些定時中斷:

// should be some "far" type function to preserve "cs" as well
far void fakeThreadSwitch() {
    asm {
        cli  ; or other means to disable thread switch (re-entry)
        ; store the current values of all registers
        pusha
        pushf
        push ds
        push es
        ; set `ds` to thread contexts data section

        ; figure out, which thread is currently running
        ; (have some "size_t currently_running = index;" in context section)

        ; if none, then pick some SLEEPING
        ; but have some [root_context] updated (.stack), so you can
        ; do final switch to it upon terminating the OS.

        ; verify the ss points to that thread stack ->
        ; if you by accident did interrupt OS kernel,
        ; then just return without touching anything (jump to "pop es")

        ; store ss:sp to [current_thread_context.stack]

        ; decide if you want to switch to some other context
        ; (or kill current) simulating "preemptive multitasking"
        ; if switch, set up all flags correctly (RUNNING/SLEEPING/index)

        ; load ss:sp from [next_thread_context.stack]

        pop es
        pop ds
        popf
        popa
        sti  ; or enable thread switch interrupt by other means
    }
}

然后在fAddress啟動一些執行代碼的新線程:

void startNewThread(void far *fAddress) {
    // allocate some new context for the new thread
    // (probably fixed array for max threads, searching for "FREE" one)
    // ... (inits fields in some struct [new_thread_context])
    // allocate some new stack memory for the new thread
    // ... (sets [new_thread_context.stack_allocated])
    // set up the stack for initial threadSwitch
    uint16_t far * stackEnd = [new_thread_context.stack]
    // reserve: es,ds + flags + all + cs:ip (to be executed) + OS exit trap (3x)
    stackEnd -= (2 + 1 + 8 + 2 + 3);
    // init the values in "stack"
    stackEnd[0] = stackEnd[1] = [new_thread_context.ds]; // es, ds
    stackEnd[2] = 0; // flags
    stackEnd[3] = stackEnd[4] = stackEnd[5] = 0; // di, si, bp
    stackEnd[6] = offset(stackEnd+11); // sp ahead of "pusha"
    stackEnd[7] = stackEnd[8] = 0; // bx, dx
    stackEnd[9] = stackEnd[10] = 0; // cx, ax
    stackEnd[11] = segment(fAddress);     // "return" to fAddress
    stackEnd[12] = offset(fAddress);
    // thread_exit_return is some trap function to handle
    // far return inside fAddress code, which would probably require
    // different design to make this truly usable (to fit C epilogue of f())
    stackEnd[13] = segment(&thread_exit_return);
    stackEnd[14] = offset(&thread_exit_return);
    stackEnd[15] = thread_id;
    [new_thread_context.stack] = stackEnd;
    // all context data are ready for context switch, mark this thread "ready"
    [new_thread_context.running] = SLEEPING;

    // now in some future the context-switch may pick this thread from
    // pool of sleeping threads, and will switch execution to it
    // (through this artificially prepared stack image)
}

一個內核處理程序,這個設計為任何f()正常完成的“登陸”點,它將返回(或顯式調用)。

void thread_exit_return() {
    // get the exited thread_id somehow
    [thread_context.running] = FINISHED;
    // deallocate [thread_context.stack_allocated]
    // deallocate thread context (marking it as "FREE"?)
}

需要更多的思考和設計如何運行內核本身(無論是在另一個線程中,還是在原始的應用程序上下文中),以及如何給它運行時間。 以及如何控制內核執行新線程,或者殺死/退出舊線程。

無論如何,這個練習的重要部分是push-all-in-thread-stack / pop-all-from-stack-stack,讓你粗略地了解搶占式多任務是如何工作的(盡管在32b受保護的操作系統中這涉及更多的技巧) CPU切換到保護層(並返回用戶域)並使用不同的堆棧作為內核等。所以只有原理相同)。

當然在16b不受保護的情況下,這是一個非常脆弱的構造,可以很容易地在不同的線程中被破壞(我很可能會疏忽一些重要的東西,所以它很可能需要一些重的bug修復才能使它工作)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM