Java虚拟机学习笔记(二)—方法调用(一)

方法调用:

    方法作为程序组成的基本单元,作为原子指令的初步封装,计算机必须支持方法调用。Java语言的原子指令是字节码,Java方法时对字节码的封装,因此JVM必须支持对Java方法的调用

取指(取出指令):

    方法对原子指令的封装,计算机进入方法后,最终逐条取出这些指令并逐条执行。JVM进入Java方法后,也要能够模拟硬件CPU,能够从Java方法中逐条取出字节码指令。

运算:

    计算机取出指令后,根据指令进行相关的逻辑运算,实现功能。

一、方法调用

    JVM作为一款虚拟机,应具备完整的执行Java程序的能力,所以必须具备执行单个Java函数的能力。从而必须能执行函数调用。JVM实际上最终调用的并不是真正的Java函数,而是其对应的一堆机器指令。(参见笔记一:在运行时将Java字节码指令动态翻译成本地机器指令)

1、真实机器的调用

    主要学习一些真实的机器调用原理。所涉及的知识较多:保存现场、堆栈分配、参数传递等。先看书中给的例程:

    作用:使用汇编进行求和(读书少,汇编语言作者尚未接触,尴尬……)

main:
	pushl %ebp
	movl%esp,	%ebp
	subl$32,	%ebp
	movl$5,	20(%esp)
	movl$3, 24(%esp)
	movl24(%esp),	%eax
	movl%eax,	4(%esp)
	movl20(%esp),	%eax
	movl%eax,	(%esp)
	calladd
	movl%eax,	28(%esp)
	
	
	movl$0,	%eax
	leave
	ret
	
	
add:
	subl$16,	%esp
	movl12(%ebp),	%eax
	movl8(%ebp),	%edx
	addl%edx, %eax
	movl%eax,	-4(%ebp)
	movl-4(%ebp), %eax
	leave
	ret

    分析该程序:该段汇编程序中定义了两个标号,一个main标号,一个是add标号。标号类似于C语言中函数的概念。当成函数即可。具体关注内存是怎样变化的。

(1)main函数详解

main:
       //保存调用者栈基地址,并为main()函数分配新栈空间。
	pushl %ebp   
	movl%esp,	%ebp
	subl$32,	%ebp//分配新栈、一共32个字节
        
        //初始化两个数据,一个是5,一个是3
	movl$5,	20(%esp)
	movl$3, 24(%esp)
        //压栈,将5和3压栈
	movl24(%esp),	%eax
	movl%eax,	4(%esp)
	movl20(%esp),	%eax
	movl%eax,	(%esp)
        //调用add函数
	calladd
	movl%eax,	28(%esp)
	
	//返回
	movl$0,	%eax
	leave
	ret

    上述过程共包含5步:保存调用者栈基地址,初始化数据,压栈,函数调用和返回。

    1.保存栈基并分配新栈

        

//保存调用者栈基地址,并为main()函数分配新栈空间。
	pushl %ebp   
	movl%esp,	%ebp

    pushl %ebp就是保存调用者的栈基地址。操作系统即为调用者。movl%esp,%ebp将调用者的栈基地址指向其栈顶。

    执行完后,subl%32,%esp指令即为分配栈空间的指令。其具体含义:将当前栈顶减去32字节的长度。因为在linux平台上,栈是向下增长的,从内存的高地址往低地址方向增长,因此每次调用一个新的函数时,需要为新的函数分配栈空间,新函数的栈顶相对于调用者函数的栈顶,内存地址一定是低位方向,因此新函数的栈顶总是通过对调用者函数的栈顶做减法而计算出来。

    一个字节包含8个二进制位,一个int型整数包含4个字节,main函数的房发展一共可以容下8个int型数据,如图:

                                    

    2.初始化数据

    main函数接下来两条指令:

    

movl$5,	20(%esp)
	movl$3, 24(%esp)

    这段程序表示,分别将5和3这两个整数保存到main()栈中,其中20(%esp)表示当前栈顶(即esp寄存器当前所指向的你村地址)往上移动20字节位置,数据5就保存到这里。同理整数3被保存到了main()函数栈顶往上偏移24字节处的位置。如图:


                               

    将main函数的站定位置记为(%esp),整个main方法栈空间32字节,每4个字节为单元划分,按照便宜了来标记main的方法栈。接下来再看5和3在main方法栈中具体的位置。如图:

                                    

       注意:栈底那个位置即28(%esp)是留给调用add()函数的返回值的。

    3.压栈

    main函数接着执行:

movl24(%esp),	%eax
	movl%eax,	4(%esp)
	movl20(%esp),	%eax
	movl%eax,	(%esp)

    这四条指令主要作用是压栈。movl24(%esp),%eax是将24(%esp)处的内存值传到eax寄存器中,即将3传进去。接着movl%eax,4(%esp)又将eax寄存器的值传送到4(%esp)这个地方。CPU不支持将数据从一个内存位置直接传送到另一个内存位置,若想实现这个效果,必须用寄存器周转。

    一般而言,往栈顶传送数据的行为叫做“压栈”。压栈操作目的即将要进行函数调用。真实的物理机器,发起函数调用之前,必定要进行压栈操作。压栈的目的是为了传参。

    4.函数调用

    压栈结束后,main函数开始进行函数调用,即call add指令。add()函数执行完,会将计算的结果保存到eax寄存器中。main函数要取得add函数返回值,便直接从eax寄存器中拿即可。因此接着执行movl%eax,28(%esp)。此时方法栈内存情况如下:

                                        

    编译器将一个方法内的局部变量分配在靠近栈底的位置,而将传递的参数分配在靠近栈顶的位置。

    5.返回
    
movl$0,	%eax
	leave
	ret

    函数返回,将返回值保存到eax寄存器中,然后执行两条例行返回指令。

(2)add()函数详解

    add()函数调用总体上分4步:保存调用者栈基地址,读取参数,执行运算,返回。

    1.保存调用者栈基地址

    

subl$16,	%esp
	movl12(%ebp),	%eax

    这两条指令主要是保存调用者栈基地址,物理机器在执行函数调用时,被调用者总是要保存调用者栈基地址。这是因为esp和ebp两个寄存器接下来要指向被调用者的栈基地址和栈顶,这两个寄存器原本保存的是调用者的栈基地址和栈顶地址,现在即将被修改,如果不保存起来,那么当被调用者函数执行完毕后,程序会返回到调用者流程中,物理机器将无法恢复调用者的栈基和栈顶,从而无法继续执行。

    接下来为add()函数分配空间 大小为16字节。subl $16, %esp。内存结构如图:

                                            

    其中eip所占用的空间:在main函数执行calladd指令时,物理机器自动往栈顶压了一个数值—eip,CPU所执行的指令位置由CS:IP这两段寄存器共同决定,这里的eip是IP寄存器。主要是为了让main函数执行完call调用回来后,能够继续处理main函数接下来的指令。下一个ebp:这个值是在执行add函数时入栈的。

    结论如下:

  •     物理机器执行call函数调用时,机器会自动将eip入栈
  •     物理机器执行函数调用时,被调用放需要手动将ebp入栈。
    2.读取参数

    

movl12(%ebp),	%eax
	movl8(%ebp),	%edx

    第一条指令使用了2个寄存器,ebp和eax,其中ebp寄存器与esp一样,只用于标识栈底位置。指令含义为:从add函数栈底向上便宜12字节的位置取出数据,将数据传送给eax寄存器。同理,第二条指令时从add函数栈底位置向上便宜8字节取出数据,放入ebx寄存器。

    3.执行运算

    

addl%edx, %eax

    将edx寄存器中的值与eax寄存器中的值相加,相加结果放入eax寄存器中。在add函数执行本指令之前,已经通过读入参数将main函数所传来的两个参数分别督导eax和ebx两个寄存器中,因此对这两个寄存器求和就是对main函数的两个参数进行求和。

    执行完求和后,add函数接着执行movl%eax,-4(%ebp),把eax寄存器中的值转移到栈基址往下偏移4个字节的位置。其实就是add函数的方法栈内的第一个位置。

    4.返回

    接下来是将结果返回,如果有返回值,就把返回值放到eax寄存器中,然后执行leave和ret指令。没有返回值直接执行上述指令。

    通过上述程序的分析,在物理机器执行函数调用时,进行以下操作:

  1. 参数入栈。有几个参数就把几个参数入栈。不同机器入栈顺序不同。
  2. 代码指针(eip)入栈,这样等被调用函数执行完毕后,物理机器可以再回来继续执行原来的函数指令。
  3. 调用函数的栈基地址入栈。为物理机器从被调用者返回调用方做准备。
  4. 为调用方分配栈空间。每一个函数都有自己的栈空间。

    物理机器在执行程序时,将程序分成若干函数,每个函数对应一段机器码。一段程序的机器码放在一块连续的内存中,这块内存叫代码段。物理机器为每一个函数分配一个方法栈,方法栈与代码段在地址上没有任何联系。并且只有当物理机器执行某个函数时,才会为其分配方法栈。

ps:下一节学习C语言的函数调用

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值