当一种方法调用另一种方法时,会发生什么?

时间:2009-08-16 20:12:30

标签: language-agnostic computer-science

这类似于What happens when you run a program?,但不是欺骗。

假设我有一个简单的控制台程序,它有两个方法A和B.

    public static void RunSnippet()
    {
        TestClass t = new TestClass();
        t.A(1, 2);

        t.B(3, 4);
    }

    public class TestClass
    {
        public void A(int param1, int param2)
        {
            //do something
            C();
        }

        private void C()
        {
            //do
        }

        public bool B(int param1, int param2)
        {
            //do something
            bool result = true;

            return result;
        }
    }

有人可以详细解释(但请用简单的英文简单说明),当RunSnippet调用方法A和方法B(并且他们在内部调用其他方法)时会发生什么。我想了解引擎盖下真正发生的事情......意思是params如何通过,它们存储在哪里,本地变量发生了什么,返回值是如何传递的,如果另一个线程在A调用C时开始运行会发生什么,如果抛出异常将会发生什么。

7 个答案:

答案 0 :(得分:10)

我不太确定你正在寻找什么程度的细节,但这是我在解释发生的事情时的准备:

  1. 为您的可执行文件创建了一个新进程。该进程有一个堆栈段,包含每个线程的堆栈,一个静态变量的数据段,一个称为堆的内存块,用于动态分配的内存,以及一个包含已编译代码的代码段。
  2. 您的代码被加载到代码段中,指令指针被设置为main()方法中的第一条指令,代码开始执行。
  3. 对象t是从堆中分配的。 t的地址存储在堆栈中(每个线程都有自己的堆栈)。
  4. t.A()通过将返回地址放在堆栈上的main()并将指令指针更改为t.A()代码的开头来调用。返回地址与值1和2一起放在堆栈上。
  5. t.A()通过将返回地址放在堆栈上的t.A()并将指令指针更改为t.C()代码的起始地址来调用t.C()。
  6. t.C()通过将返回地址弹出到堆栈的t.A()并将指令指针设置为该值来返回。
  7. t.A()以与t.C()类似的方式返回。
  8. 对t.B()的调用与对t.A()的调用非常相似,只是它返回一个值。返回该值的确切机制取决于语言和平台。通常,该值将在CPU寄存器中返回。
  9. 注意:由于您的方法非常小,现代编译器通常会“内联”它们而不是进行经典调用。内联意味着从方法中获取代码并将它们直接注入到main()方法中,而不是通过进行函数调用的(轻微)开销。

    鉴于你的例子,我没有看到线程如何直接进入图片。如果您第二次启动可执行文件,它将在新进程中运行。这意味着它将获得它自己的代码段,数据段和堆栈段完全将它与第一个进程隔离开来。

    如果你的代码是在一个在几个线程上调用main()的大型程序中运行的,那么它几乎就像前面描述的那样运行。代码是线程安全的,因为它不访问任何可能的共享资源,例如静态变量。线程1无法“看到”线程2,因为所有关键数据(值和指向对象的指针)都存储在线程的本地堆栈中。

答案 1 :(得分:4)

函数调用本质上是一个goto语句,除了最后,它必须返回到调用它的位置。

有一个函数调用堆栈,它基本上包含有关“返回”的位置的信息。

函数调用需要:

  • 将当前指令的位置存储(推送)到堆栈上,以便被调用的函数在完成时使用。
  • 同时将所有参数推送到堆栈
  • 转到被调用函数中的第一条指令。

当被调用函数需要读取参数时,它将从堆栈中读取它们。

当被调用的函数完成或命中“return”语句时,它会找到它需要返回的地址,然后“转到”它。

答案 2 :(得分:2)

(假设是x86) 首先,您必须了解堆栈。 函数使用称为“堆栈”的内存区域。您可以将其视为一堆板,其中每个板包含一个DWORD(32位)数据。 CPU中有一个寄存器,用于跟踪我们正在处理的堆栈中的当前位置(它只是一个虚拟内存地址)。它被称为堆栈指针,通常存储在esp寄存器中。

当功能与堆栈交互时,它们通常执行以下两项操作之一:推送 pop 。 “推”是指它放在堆栈顶部的东西,包括将堆栈指针移动到下一个最高位置,然后将某些东西复制到新位置(新顶部)。推送“增加堆栈”,因为现在存储的数据更多(更多的板块)。

“pop”是指堆栈中最顶层的项目被“删除”,包括将当前堆栈顶部的任何内容(由esp寄存器指向)复制到cpu寄存器(通常为eax)然后将堆栈指针移动到堆栈中较低的一个位置。

现在我们可以谈谈设置调用函数。

<强>码

t.B(3, 4);

<强>组件

// here is a push we described above. The function we are in currently is
// pushing the value "4" onto the stack. This is one of the arguments to the
// B function we are calling. Note that we push the last argument first
push 4 
// here is another push. This time we are pushing the next argument to the 
// B function
push 3
call B  // this call sets up the context for the next function to run

当发生调用时,我们正在将上下文从当前函数转换为被调用的函数。函数需要运行的额外peices信息是我们推入堆栈的参数。

新函数现在将与堆栈保持一致,为其所具有的局部变量腾出空间,并将堆栈指针保存到寄存器中,以便在函数返回后可以恢复它。如果没有发生这种情况,那么当它重新获得控制而不知道如何访问它先前放入堆栈的东西时,调用函数将会迷失方向,例如它自己的局部变量或者堆栈指针的上下文。调用它的函数。

现在,这是在集会中发生的事情(从Havenard偷来的)。

// Here is the B function making sure that the calling function can get back to
// the it's stack context when B returns. 
push ebp
mov ebp, esp
// remember when I said that a push was growing the stack. Well you can also grow
// it just by moving the stack pointer higher, as if there were already more plates there
// you may wonder why we are subtracting (sub) from the stack pointer (esp) to grow it
// the reason is that the stack "grows down" in memory. In other words, as the stack grows
// the memory addresses of the stack grow smaller.
// the reason we are subtracting 4 is because we only need to grow the stack by one plate
// so that we can store the local variable 'result' there. If we had 2 local variables
// we would have subtracted 8
sub esp, 4 
// the instructions below are simply moving the static value 1 into the local variable
// 'result'. Local variables are always referenced relative to the bottom of the stack
// context for the current function. This value is stored in the ebp register, which we
// saw earlier in the function setup above.
// so now we think of the location where the 'result' variable is stored as "ebp-4"
// we know that because we put it there.
mov dword ptr [ebp-4], 1 // result = 1 (true)
// eax is a special register that contains the return value of the function. That is why
// you see the value of 'result' (which we know as [ebp-4] in the eax register
mov eax, dword ptr [ebp-4]
// We adjust the stack pointer back to it's previous location 
// before we subtracted to make room for our local variable
add esp, 4
// Our work is done now.. time to clean stuff up for our calling function and 
// leave things as we found them. Our trusty ebp register stores the old stack pointer
// that our calling function needs to resume it's stack context.
mov esp, ebp
pop ebp
ret

我确信我遗漏了一些细节,特别是从B函数返回时,但我认为这是一个非常好的概述。

答案 3 :(得分:1)

您的意思是汇编语言级别,还是操作系统级别?

就汇编而言,调用方法时会发生的事情是所有参数都被压入堆栈,最后是方法的地址(如果它是虚拟的,则有额外的表查找)。然后代码从方法的地址继续,直到命中“ret”指令并从执行调用的地方继续执行。您应该学习汇编以及如何编译C以获得对该过程的良好控制。

在操作系统级别,调用方法没有什么特别之处,所有操作系统都会为进程分配CPU时间,并且该进程负责在此期间执行它想要的操作,无论是调用方法还是其他任何方法。但是,线程之间的切换是由OS完成的(与使用CPython中的软件线程不同)。

答案 4 :(得分:1)

如果您对大会级别的解释感兴趣,我建议您在CS107 @Stanford大学观看此讲座。我发现它能够以非常非常简单的英语方式准确地解释函数调用的成本。

http://www.youtube.com/watch?v=FvpxXmEG1F8&feature=PlayList&p=9D558D49CA734A02&index=9

答案 5 :(得分:0)

  

TestClass t = new A();

我认为你的意思是新的TestClass()。

至于幕后发生的事情,编译器会将此代码转换为Java字节码。以下是article关于“ Java虚拟机如何处理方法调用并返回”的摘录。

  

当Java虚拟机调用时   一个类方法,它选择方法   根据类型调用   对象引用,总是如此   在编译时已知。在另一   当虚拟机调用时   一个实例方法,它选择   基于实际调用的方法   对象的类,可能只是   在运行时已知。

     

JVM使用两种不同的方式   说明,如下所示   表,调用这两个不同的   各种方法:invokevirtual for   实例方法和invokestatic   类方法。

     

invokevirtual和。的方法调用   invokestatic   操作码操作数描述

     

invokevirtual indexbyte1,indexbyte2   pop objectref和args,调用方法   在常量池索引

     

invokestatic indexbyte1,indexbyte2   pop args,调用静态方法   常数池索引

答案 6 :(得分:0)

t.B(3, 4)做了什么:


push 4
push 3
call B
add esp, 8 // release memory used

call在调用之后立即将指令的地址压入堆栈,然后将进程线程跳转到B()地址:


push ebp // save EBP state, the caller will need it later
mov ebp, esp // save ESP state
// push registers I would use but EAX, I'm not using any
sub esp, 4 // alloc 4 bytes in the stack to store "result"
mov dword ptr [ebp-4], 1 // result = 1 (true)
mov eax, dword ptr [ebp-4] // prepares return value o be "result"
add esp, 4 // frees allocked space
// pop registers
mov esp, ebp
pop ebp
ret

共享对象实现。声明新对象时,所有存储的都是对象变量。在这种情况下,没有引用。

关于多个线程,线程内存是分开的。当内核将处理器切换到另一个线程时,线程流中没有任何事情发生。内核只是冻结并恢复此流程。