generate_call_stub()保存调用者方法栈

   

目录

入参位置

物理寄存器Register类

保存调用者方法栈

栈空间对接


       在总结generate_call_stub()保存调用者方法栈过程之前,还是先来回顾下,JVM初始化链是怎么到达该函数的。call_stub()函数里返回一个CallStub类型的函数指针,由_call_stub_entry这一基本类型转换而得到,_call_stub_entry在前面的日志里说到,它最终是unsigned int类型,存放的是某一内存地址,类型转换为CallStub后,也就是call_stub()会将返回值函数指针所存放的地址,标识的是某一个函数,直接调用。_call_stub_entry变量存放的内存地址,在JVM初始化时就会指定好,上一篇日志中给出的初始化链最后,调用了generate_initial()函数,函数内部又调用了generate_call_stub(),该函数就是对_call_stub_entry变量进行初始化。初始化过程中,需要将一些参数进行压栈,也就是保存到方法栈中,前面的日志里说到CallStub需要的参数有8个,连接器link、函数返回地址result_val_address、函数返回类型result_type,Java方法method()和entry_point例程入口等等,从generate_call_stub()函数中我们也看到了初始化参数的压栈操作。

 

入参位置

      可以看到,在generate_call_stub()初始化时,会对一系列的参数进行定义,之后压栈,那么它们的入参位置在哪呢?从它们后面的参数可以看到,link的入参位置在rbp栈基地址寄存器往上偏移2*wordSize的地方,在32位的系统环境下,wordSize表示4个字节,64位系统下表示8个字节,以32位为例,所以第一个参数link的栈地址是8(%ebp),result栈地址是3*4为12(%ebp),下面的以此类推,待所有参数初始化好后,它们在栈中的位置就变成了下图所示:

      像8(%ebp),12(%ebp)这样的位置标记是汇编语言的方式,在JVM中入参位置用4个字节(32位系统下)为一个单位长度来表示,所以第一个参数link,汇编表示8(%ebp),JVM表示为2N(%ebp),result汇编表示12(%ebp),JVM表示为3N(%ebp),这里的N表示为4个字节。最后,CallStub里的八个参数便依次从2N表示到9N,对应上图从2*wordSize到9*wordSize。

 

物理寄存器Register类

      从上面的generate_call_stub()函数中可以看到,无论是哪一个参数初始化,最后都是返回Address类型的变量,Address类表示的是一个物理寄存器,是一个C++类,里面有保存该寄存器的栈基地址和偏移量信息,结构如下:

      Registert类标识一个物理寄存器,用来存放栈基地址,从上面的初始化信息可以看到CallStub里那几个参数都传入rbp栈基地址寄存器,作用是确定参数地址和恢复调用方法的栈位置。第二个参数int型的_disc,标识的就是偏移量,通过rbp寄存器和相对于其的偏移量来确定入参位置,清楚明白。有了这个C++类后,就可以对参数进行寻址了,例如想寻找method参数在栈中的地址,可以直接:

Address position(rbp, 5N);

回到generate_call_stub()函数中,wordSize在JVM中也是有定义的,它的类型是int:

const int wordSize = sizeof(*char);

      这就很好对应了前面说的,在32位操作系统环境下,wordSize大小为4个字节,因为一个char类型指针大小为4,sizeof(*char)便返回4,在64伟系统环境下,char型大小为8个字节,所以wordSize等于8。从前面的generate_call_stub()函数中看有的参数初始化栈位置是正数*wordSize,有的是负数*wordSize,前面说过,如果根据rbp寄存器来确定栈位置,在寄存器往上偏移的地方,数字为正,如果是负数*wordSize,则表示在寄存器往下偏移多少个字节的位置。

 

保存调用者方法栈

继续沿着generate_call_stub()函数往下走,在一系列参数初始化后,下一个要执行的是_entrer()函数

该函数的定义如下:

      看到这里是不是很熟悉?(虽然我前面日志没总结过函数汇编代码的执行过程,因为汇编代码我还不太熟,但是如果你熟悉汇编代码函数调用过程,就会明白这是怎么回事)这就是函数调用前会执行的保存调用者栈基地址和重新指定栈基地址的指令,因为在generate_call_stub()函数中需要调用_entrer()函数,那么首先进行push(rbp)操作,保存当前调用者函数的栈基位置,接着mov(rbp, rsp),让rbp寄存器指向rsp寄存器的位置,也就是generate_call_stub()函数的栈顶,这样就可以往下为其他调用函数分配栈空间,_enter函数的作用就是这样,两条代码翻译成汇编指令就是:

push %ebp

mov %esp, %ebp

在每一个函数调用之前,都会执行这两条指令。

 

栈空间对接

      寄存器指向栈顶位置后,接下来就应该为被调用者函数分配方法栈空间了,rbp寄存器当前指向了调用者函数的栈顶位置,其实也就指向了被调用者函数的栈底位置:

      接下来为被调用者分配的方法栈空间,将会“对接”在调用者方法栈栈顶处,也就是从调用者方法栈顶处开始往下为被调用函数分配栈空间,依靠的汇编指令是sub operand, %sp,该指令为栈空间扩展容量,例如:

sub $32 %esp

      该汇编指令表示分配大小为32字节的栈空间,并将esp寄存器指向栈顶位置。这种对接方式,或者可以理解为从原本的方法栈下扩展了容量,可以让被调用函数很方便地对调用者函数中压栈的参数进行入参,因为有ebp寄存器的存在,还记得它当前指向的位置吗,就在调用者函数的栈顶,也同时是被调用者函数的栈底处,如果被调用者函数想要拿调用者函数中的参数进行入参,那么可以通过ebp寄存器来进行定位,获得调用者函数方法栈中的压栈参数位置:

movl8(%ebp), %eax

movl12(%ebp), %edx

      拿这两条指令举例,8(%ebp)表示取出ebp寄存器往上偏移8个字节位置的数据,保存到eax寄存器中,ebp寄存器此时指向的是调用者函数的栈顶,所以往上偏移就是从自己的方法栈内读取数据。12(%ebp)同理,表示从ebp寄存器往上偏移12个字节位置处,取出数据,放到edx寄存器中,这两条指令通常是在被调用者函数里面的,由于需要入参,取出数据后,被调用者函数便可以那参数进行其他操作,例如如果想把两者相加,那么可以执行指令:

addl %eax, %edx

      表示将两个寄存器内的数据进行相加,结果保存到edx寄存器处,这就是被调用者函数读参过程,得益于JVM这种对接栈空间的分配方式,调用者和被调用这函数之间可以通过寄存器很好地定位参数的栈位置,然后在两个函数之间传递。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章