Language agnostic 当一个方法调用另一个方法时,会发生什么?

Language agnostic 当一个方法调用另一个方法时,会发生什么?,language-agnostic,computer-science,Language Agnostic,Computer Science,这类似于,但不是欺骗 假设我有一个简单的控制台程序,有两种方法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) {

这类似于,但不是欺骗

假设我有一个简单的控制台程序,有两种方法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(他们在内部调用其他方法)时,会发生什么情况。我想了解引擎盖下到底发生了什么…这意味着参数是如何传递的,它们存储在哪里,本地变量会发生什么,返回值是如何传递的,如果A调用C时另一个线程开始运行会发生什么,如果抛出异常会发生什么。

我不太确定您要查找的详细程度,但我想解释一下发生了什么:

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

    以你的例子来说,我不明白线程是如何直接进入画面的。如果再次启动可执行文件,它将在新进程中运行。这意味着它将获得自己的代码段、数据段和堆栈段,并将其与第一个进程完全隔离


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

    您是指汇编语言级别还是操作系统级别

    就汇编而言,调用方法时会发生的情况是,所有参数都被推送到堆栈上,最后是方法的地址(如果是虚拟的,则会有额外的表查找)。然后,代码从方法的地址继续执行,直到命中“ret”指令并从调用的位置恢复执行。您应该学习汇编以及如何编译C,以便更好地掌握该过程

    在操作系统级别,调用该方法没有什么特别之处,操作系统所做的只是将CPU时间分配给进程,而该进程负责在这段时间内做它想做的事情,不管是调用方法还是其他什么。但是,线程之间的切换是由操作系统完成的(不像您在CPython中使用软件线程)

    TestClass t=新的A()

    我想你指的是新的TestClass()

    至于在后台发生的事情,编译器将把这些代码转换成Java字节码。下面是一篇关于“Java虚拟机如何处理方法调用和返回”的文章的摘录

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

    JVM使用两种不同的方法 说明,如下所示 表,以调用这两个不同的 方法的种类:invokevirtual for 实例方法,以及 类方法

    invokevirtual和的方法调用 不动产 操作码操作数说明

    调用虚拟索引字节1,索引字节2 pop objectref和args,调用方法 在恒定池索引下

    invokestatic索引字节1,索引字节2 弹出参数,在 恒定池索引


    函数调用本质上是一个goto语句,只是在最后,它必须返回到调用它的地方

    有一个函数调用堆栈,它本质上保存了关于“返回”位置的信息

    函数调用需要:

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

    当被调用的函数完成,或点击“return”语句时,它会找到它需要返回的地址,并找到它的“goto”

    t.B(3,4)的作用是:

    
    push 4
    push 3
    call B
    add esp, 8 // release memory used
    
    call
    在调用后立即推送指令地址
    
    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
    
    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
    
    // 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