汇编代码导致递归

Assembly code causes recursion

本文关键字:递归 代码 汇编      更新时间:2023-10-16

我一直在编写一个C应用程序,我需要x86汇编语言。我对汇编很陌生,下面的代码片段会导致递归:

unsigned int originalBP;
unsigned fAddress;
void f(unsigned short aa) {
    printf("Function %dn", aa);
}
unsigned short xx = 77;
void redirect() {
    asm {
        pop originalBP
        mov fAddress, offset f
        push word ptr xx
        push fAddress
        push originalBP
    }
}

如果我调用redirect,它将重复输出:"Function 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-fff6) mov 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
  • (ffe - fff6) pop bp
  • ret (FFF0-04AC)
    • (in f) (FFF2-04E6) push bp
    • (fff0-fff6) mov bp,sp
    • printf执行
    • (fff0-fff6) pop bp
    • ret (FFF2-04E6)下一个语句似乎是return 0;,这是main的结束。

执行继续通过一堆行,并以某种方式返回到调用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
对于前一种配置,参数aa包含redirect的返回地址,f的返回地址为xx的值。


根据你对我的评论的回答,代码意外循环了。


如果你想用参数调用f,一定要把它们放在返回地址之前:

pop originalBP
pop originalRIP
;Arguments go here    
push xx
push originalRIP
push fAddress
push originalBP

您没有发布用于编写redirect的编译器和编译选项。

随着优化的开启,你不能假设完整的C函数序言/尾声将被使用,所以你在没有任何布局的情况下操作堆栈(如果有零序言/尾声,那么你确实在返回地址之前注入了2个值给调用者,所以重定向只会返回调用者(主?)这可能本质上只是退出->不调用f =不是你的情况)。

在asm块中你已经有了fn地址,为什么不直接调用它呢?栈是这样的:有人调用redirect -> redirect调用某个地址->地址fn() ->返回到redirect ->返回到调用者

它看起来像你试图修改它:有人调用重定向->重定向调用一些地址->地址fn() ->返回调用者(跳过返回重定向)。由于重定向后记只是一小段代码,我看不出这种修改有什么好处(我也看不出它是如何与"上下文切换"相关的)。

无论如何,检查你的编译器选项如何生成最终代码的汇编清单,看看它是如何真正编译的,或者更好的是,使用调试器检查它(在汇编级别上每一步指令)。


编辑(提供调试信息后):

当你到达return 0时,在堆栈中注入了额外的外来xx (sp0xFFF4)而不是sp是指向0的原始FFF6

main的结束可能没有正确处理这个(做pop bp ret我猜),假设sp是正确的返回。(它会做其他的C尾声,包括mov sp,bp,它可能会在你的堆栈篡改中幸存下来)。

然后,如果它会在所有函数中做其他尾声,它也会在redirect()中做,所以你必须修改bp,也使redirect()的末尾做retfAddress。像dec bp, dec bp这样可能就足够了,因为您通过向参数空间注入2B来增加堆栈。

当main中的return 0被击中时,再检查一次调试,它是如何实现的,它是否可以处理修改后的sp(好吧,显然它不能,因为它意外地循环到redirect)。

如果是这种情况,您可能应该修补main以在return 0;之前恢复sp。我想知道是否简单的mov sp,bp会做(bp应该是FFF6在那之前)。

结论:在多个调用中篡改堆栈帧总是很棘手的事情。;)

那么,你正在朝着这样的方向前进吗?(因为我有点不能确切地把我的手指放在你的代码从问题将被使用,似乎是基本的堆栈练习,让你知道如何影响代码执行,这将后来演变成这样的东西…也许……

在一些类似c的伪代码中,在16b中伪造上下文切换(好,更像注释:)),必须安装为一些定时中断:

// 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-other stack,让你大致了解抢占式多任务是如何工作的(尽管在32b保护的操作系统中,这涉及到更多的技巧,CPU切换到保护层(并返回到用户层),并为内核使用不同的堆栈,等等。所以只有原理是一样的)。

当然,在16b未受保护的情况下,这是一个非常脆弱的结构,它可以很容易地在不同的线程中被破坏(我很可能确实忽略了一些重要的事情,所以它很可能需要一些严重的错误修复来使它工作)。