IL有一些操作参数的操作代码,如Ldarg.0、Ldarg.1等。
我知道,在执行call操作码之前,这些参数被推入堆栈,在某些情况下,Ldarg.0用于获取对this的引用(例如成员)。
我的问题是:当调用启动时,这些参数存储在哪里?从执行的调用中可以访问调用方堆栈的副本吗?
我在哪里能找到更多关于那个主题的信息?
更新
我知道虚拟机是抽象的,JIT编译器解决了这些问题,但是让我们想象一下,如果IL被解释,就像在.NET微框架上那样
发布于 2013-05-06 14:34:42
MSIL使用虚拟机器的规范。传递给方法的参数的心理模型是它们存在于数组中。其中,Ldarg从数组中选择一个元素来访问方法参数,并将其推送到计算堆栈上。Opcodes.Ldarg_0是一个更通用的Opcodes.Ldarg IL指令的简写版本,它总是通过选择元素0来节省两个字节。对于第二个论点,Opcodes.Ldarg_1也有同样的想法。当然,非常常见的情况是,Ldarg只在方法有4个以上的参数时才会“昂贵”。强调双引号,这不是你所担心的那种费用。
运行时参数的实际存储非常不同。这取决于您使用的抖动,不同的体系结构使用不同的方式传递参数。通常,前几个参数通过cpu寄存器传递,其余通过cpu堆栈传递。像x64或ARM这样的处理器有很多寄存器,所以使用寄存器传递的参数比x86多。受该体系结构的clrcall呼叫约定规则的控制。
发布于 2013-05-06 17:04:03
IL (现在称为CIL,公共中间语言,而不是MSIL)描述了想象中的堆栈机器上的操作。JIT编译器接受IL指令并将其编译成机器代码。
在调用方法时,JIT编译器必须遵守调用约定。此约定指定如何将参数传递给被调用的方法,如何将返回值传递给调用方,以及谁负责从堆栈(调用方或被调用方)中删除参数。在本例中,我使用cdecl调用约定,但实际的JIT编译器使用其他约定。
一般方法
具体细节取决于实现,但是.NET和Mono编译器用于将CIL编译成机器代码的一般方法如下:
当然,在这些步骤之间有很多优化。
示例
让我们举一个例子来解释这些步骤:
ldarg.1 // Load argument 1 on the stack
ldarg.3 // Load argument 3 on the stack
add // Pop value2 and value1, and push (value1 + value2)
call int32 MyMethod(int32) // Pop value and call MyMethod, push result
ret // Pop value and return在步骤1中,IL被转化为基于寄存器的操作(operation dest <- src1, src2):
ldarg.1 %reg0 <- // Load argument 1 in %reg0
ldarg.3 %reg1 <- // Load argument 3 in %reg1
add %reg0 <- %reg0, %reg1 // %reg0 = (%reg0 + %reg1)
// Call MyMethod(%reg0), store result in %reg0
call int32 MyMethod(int32) %reg0 <- %reg0
ret <- %reg0 // Return %reg0然后将其转化为机器指令,例如x86:
mov %reg0, [addr_of_arg1] // Move argument 1 in %reg0
mov %reg1, [addr_of_arg3] // Move argument 3 in %reg1
add %reg0, %reg1 // Add %reg1 to %reg0
push %reg0 // Push %reg0 on the real stack
call [addr_of_MyMethod] // Call the method
add esp, 4
mov %reg0, eax // Move the return value into %reg0
mov eax, %reg0 // Move %reg0 into the return value register EAX
ret // Return然后为每个虚拟寄存器%reg0,%reg1 1分配一个机器寄存器。例如:
mov eax, [addr_of_arg1] // Move argument 1 in EAX
mov ecx, [addr_of_arg3] // Move argument 3 in ECX
add eax, ecx // Add ECX to EAX
push eax // Push EAX on the real stack
call [addr_of_MyMethod] // Call the method
add esp, 4
mov ecx, eax // Move the return value into ECX
mov eax, ecx // Move ECX into the return value register EAX
ret // Return溢流
通过仔细选择寄存器,可以消除一些mov指令。当代码中的任何一点使用的虚拟寄存器多于可用的机器寄存器时,必须溢出一个机器寄存器才能使用。当机器寄存器溢出时,插入指令将寄存器的值推到实际堆栈上。稍后,当必须再次使用溢出值时,将插入指令,将寄存器的值从实际堆栈中弹出。
结论
如您所见,机器代码使用实际堆栈的频率与IL代码使用评估堆栈的频率几乎相同。原因是机器寄存器是处理器中最快的内存元素,所以编译器试图尽可能最好地使用它们。只有当机器寄存器短缺时,或者当该值需要在堆栈上时(例如,由于调用约定),值才会存储在真正的堆栈上。
发布于 2013-05-06 14:16:57
ECMA-335可能是一个很好的起点。
例如,第一.12.4.1节如下:
CIL代码生成器发出的指令包含足够的信息,以便CLI的不同实现可以使用不同的本机调用约定。所有方法调用初始化方法状态区域(见§I.12.3.2)如下:
一.12.3.2:
每个方法状态的一部分是一个包含局部变量的数组和一个包含参数的数组。与计算堆栈一样,这些数组的每个元素都可以保存任何单个数据类型或值类型的实例。这两个数组都从0开始(即第一个参数或局部变量编号为0)。局部变量的地址可以使用ldloca指令计算,参数的地址可以使用ldarga指令计算。 与每个方法相关联的元数据指定:
CLI为目标体系结构插入适当的填充。也就是说,在一些64位结构中,所有局部变量都可以对齐,而在另一些结构上,它们可以对齐8位、16位或32位。CIL发生器不应假设数组中局部变量的偏移量。事实上,CLI可以自由地重新排序局部变量数组中的元素,不同的实现可能选择以不同的方式对它们进行排序。
然后在分区III中,对callvirt的描述(就像一个例子一样)有:
在调用方法之前,
callvirt会弹出计算堆栈中的对象和参数。如果该方法具有返回值,则在方法完成时将其推送到堆栈上。在被调用方,obj参数作为参数0访问,arg1作为参数1访问,依此类推。
现在,这都是在规范级别。实际的实现很可能决定只让函数调用继承当前方法堆栈的前n个元素,这意味着参数已经在正确的位置了。
https://stackoverflow.com/questions/16400547
复制相似问题