C# IL和参数

C# IL和参数,c#,clr,cil,il,C#,Clr,Cil,Il,IL有一些操作码用于使用参数进行操作,例如Ldarg.0,Ldarg.1等等 我知道在执行调用操作码之前,这些参数会被推到堆栈上,在某些情况下,Ldarg.0用于获取对此的引用(例如成员) 我的问题是:当启动调用时,这些参数存储在哪里?调用方堆栈的副本是否可以从执行的调用中访问 我在哪里可以找到关于那个主题的更多信息 更新 我知道虚拟机是抽象的,JIT编译器会处理这些问题,但是让我们想象一下,如果IL被解释,就像在.NET Micro Framework上一样,可能是一个很好的起点 例如,第I.

IL有一些操作码用于使用参数进行操作,例如
Ldarg.0
Ldarg.1
等等

我知道在执行
调用
操作码之前,这些参数会被推到堆栈上,在某些情况下,
Ldarg.0
用于获取对
的引用(例如成员)

我的问题是:当启动调用时,这些参数存储在哪里?调用方堆栈的副本是否可以从执行的调用中访问

我在哪里可以找到关于那个主题的更多信息

更新

我知道虚拟机是抽象的,JIT编译器会处理这些问题,但是让我们想象一下,如果IL被解释,就像在.NET Micro Framework上一样,可能是一个很好的起点

例如,第I.12.4.1节规定:

CIL代码生成器发出的指令包含足够的信息,供CLI的不同实现使用不同的本机调用约定。所有方法调用初始化方法状态区域(见§I.12.3.2),如下所示:

  • 调用方将传入参数数组设置为所需的值
  • 对于对象类型和包含对象的值类型中的字段,局部变量数组始终为null。此外,如果 localsinit标志在方法头中设置,然后是局部变量 数组对于所有整数类型初始化为0,对于所有整数类型初始化为0.0 浮点类型。值类型不是由CLI初始化的,而是 验证的代码将提供对初始值设定项的调用,作为 方法的入口点代码
  • 计算堆栈为空
  • 而I.12.3.2有:

    每个方法状态的一部分是一个包含局部变量的数组和一个包含参数的数组。与求值堆栈一样,这些数组的每个元素都可以保存任何单个数据类型或值类型的实例。两个数组都从0开始(即,第一个参数或局部变量编号为0)。局部变量的地址可以使用ldloca指令计算,参数的地址可以使用ldarga指令计算

    与每个方法关联的元数据指定:

    • 输入方法时是否初始化本地变量和内存池内存
    • 每个参数的类型和参数数组的长度(有关变量参数列表,请参见下文)
    • 每个局部变量的类型和局部变量数组的长度
    CLI根据目标体系结构的需要插入填充。也就是说,在某些64位体系结构上,所有局部变量都可以是64位对齐的,而在其他体系结构上,它们可以是8位、16位或32位对齐的。CIL生成器不得对阵列内局部变量的偏移量进行任何假设。事实上,CLI可以自由地对局部变量数组中的元素重新排序,不同的实现可能会选择以不同的方式对它们进行排序

    然后在分区III中,
    callvirt
    (作为一个示例)的描述如下:

    callvirt
    在调用方法之前,从计算堆栈中弹出对象和参数。如果方法具有返回值,则在方法完成时将其推送到堆栈上。在被叫方,obj参数作为参数0访问,arg1作为参数1访问,依此类推


    现在,这一切都处于规范级别。实际的实现很可能决定只让函数调用继承当前方法堆栈的前n个元素,这意味着参数已经在正确的位置。

    MSIL使用虚拟机的规范。传递给方法的参数的心智模型是它们存在于数组中。其中,Ldarg从该数组中选择一个元素来访问方法参数,并将其推送到计算堆栈上。Opcodes.Ldarg_0是更通用的Opcodes.Ldarg IL指令的缩写版本,它通过始终拾取元素0来节省两个字节。对于第二个参数,Opcodes.Ldarg_1也有相同的想法。当然,非常常见的是,Ldarg只有在方法有4个以上参数时才会变得“昂贵”。强调双引号,这不是你所担心的那种费用

    运行时参数的实际存储非常不同。这取决于您使用的抖动,不同的体系结构使用不同的方式传递参数。通常,前几个参数通过cpu寄存器传递,其余参数通过cpu堆栈传递。像x64或ARM这样的处理器有很多寄存器,所以使用寄存器传递的参数比x86多。受该体系结构的规则控制。

    IL(现在称为CIL,公共中间语言,而不是MSIL)描述了虚拟堆栈机器上的操作。JIT编译器接受IL指令并将其编译成机器代码

    调用方法时,JIT编译器必须遵守调用约定。此约定指定如何将参数传递给被调用的方法,如何将返回值传递回调用方,以及谁负责从堆栈中删除参数(调用方或被调用方)。在本例中,我使用调用约定,但实际的JIT编译器使用其他约定

    一般方法 具体细节取决于实现,但.NET和Mono JIT编译器用于将CIL编译为机器代码的一般方法如下:

  • “模拟”堆栈,并使用它将所有基于堆栈的操作转换为虚拟寄存器(变量)上的操作。理论上有无限多个虚拟寄存器
  • 将所有IL指令转换为等效的机器指令
  • 将每个虚拟寄存器分配给一个实机寄存器。可用的机器寄存器数量有限。例如,32位x86体系结构只有8个机器注册表
    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
    
    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
    
    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
    
    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