汇编代码导致递归
Assembly code causes recursion
我一直在编写一个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插件
redirect
被main
调用为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的结束。
- (in f) (FFF2-04E6)
执行继续通过一堆行,并以某种方式返回到调用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
(sp
是0xFFF4
)而不是sp
是指向0
的原始FFF6
。
main
的结束可能没有正确处理这个(做pop bp
ret
我猜),假设sp
是正确的返回。(它会做其他的C尾声,包括mov sp,bp
,它可能会在你的堆栈篡改中幸存下来)。
然后,如果它会在所有函数中做其他尾声,它也会在redirect()
中做,所以你必须修改bp
,也使redirect()
的末尾做ret
到fAddress
。像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未受保护的情况下,这是一个非常脆弱的结构,它可以很容易地在不同的线程中被破坏(我很可能确实忽略了一些重要的事情,所以它很可能需要一些严重的错误修复来使它工作)。
- 如何使用递归循环我的代码(当用户输入无效输入时,它会再次提示他们)?
- 理解递归代码的演练
- 我遇到了这个代码片段,不明白. 它递归检查 C++ 字符串中是否存在大写字符
- 为什么我的递归代码给出 sigsegv 错误?
- 以下递归代码的时间复杂度是多少?
- 弄清楚这段代码是怎么回事(递归下降案例研究)
- Fibbonaci 递归代码返回错误值,始终返回下一个数字
- 我的递归可以吗?是否有任何破坏代码的示例?
- 我的编辑距离递归代码中的字符类型有问题
- 该递归代码用于计算指数的运行时间是多少
- 递归代码快速排序
- C 递归代码迭代
- 如何确定递归代码的Big-O
- 这个递归代码有什么问题
- 跟踪此递归代码
- 以迭代方式编写递归代码
- 递归代码上的运行时错误
- 如何将递归代码更改为迭代形式
- 为什么我的递归代码会导致堆栈溢出错误
- 查找无序后继递归代码中的段错误