frameobject
python虚拟机编译文件后,PyCodeObject存储了程序所有的静态信息和字节码指令,但由于python虚拟机实际上在模拟操作系统执行可执行程序的过程,但在PyCodeObject中不包括程序执行环境等信息,所以python的执行操作实际上不在PyCodeObject,而在PyFrameObject对象上。
模拟CPU栈帧
栈帧
#include "stdio.h"
int f(int a, int b){
return a+b;
}
int main(){
int c = 0;
f(1, 2);
}
- 当程序执行到函数f时,当前帧为函数f的栈帧,调用者的帧是函数main的栈帧。每调用一个函数,程序都会创建一个栈帧,每个函数都在自己的栈帧中活动,在自己的栈帧中完成对局部变量的操作。
- 运行时栈的地址空间通常从高地址向低地址延时,当函数main调用函数f时,系统就会在main函数的栈帧后创建一个新的栈帧,main函数的栈帧地址也会被保存。
c语言汇编后结果如下,我们从标号main:开始看。
.file "test.c" .text .globl f .type f, @function f: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl -8(%rbp), %eax movl -4(%rbp), %edx addl %edx, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size f, .-f .globl main .type main, @function main: .LFB1: .cfi_startproc pushq %rbp ;rbp是指向当前栈桢底部的基指针,rsp是指向当前栈桢顶部的堆栈指针。 .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $0, -4(%rbp) ;寄存器的寻址操作来的,括号外面允许提供一个偏移量,即访问指针指向内存,可加适当的偏移量,无需改变指针值 movl $2, %esi movl $1, %edi call f leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1: .size main, .-main .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)" .section .note.GNU-stack,"",@progbits
- .cfi_开头的汇编指示符用来告诉汇编器生成相应的DWARF调试信息,主要是和函数有关。
pushq %rbp
:main函数一定也是被某个函数调用的,此代码保存了调用main的函数的栈桢底部的地址。movq %rsp, %rbp
:跟新当前栈桢指向顶部的堆栈指针。subq $16, %rsp
:减法运算,使rsp指针向下偏移16字节,变量c占4字节,call会压入cs:ip的地址,占8字节,pushq %rbp
会占4字节,共4+12+4=16字节。movl $0, -4(%rbp)
:将c变量放入内存中。movl $2, %esi
,movl $1, %edi
:常数送入寄存器。call f
:调用函数f,压入cs:ip地址。pushq %rbp
:保存了main函数的栈桢底部的地址。movq %rsp, %rbp
:寄存器rsp的值移入rbp,跟新当前栈桢指向顶部的堆栈指针,在逻辑上,新的栈帧已经创建了。movl %edi, -4(%rbp)
,movl %esi, -8(%rbp)
,movl -8(%rbp), %eax
,movl -4(%rbp), %edx
,addl %edx, %eax
:把数据放入内存,然后又放入寄存器,执行加法运算,结果存储在%eax中。popq %rbp
:弹出栈值,放入rbp中,准备返回到main函数中。ret
:返回,弹出栈值,放入cs:ip中。leave
:等效于movl %ebp, %esp
,popl %ebp
,先恢复原栈顶指针,然后再根据栈顶指针恢复原栈帧的ebp。ret
:返回指令。
PyFrameObject
PyCodeObject包含了程序的关键信息字节码及静态信息,但PyCodeObject不可能包含程序的动态信息,即python的执行环境信息。在以下,代码中,print(num)
的字节码一定是一样的,但会产生不同的结果,这正是由于两个print所在执行环境不同造成的。
num = 0
def func():
num = 1
print(num)
func()
print(num)
以下可以看出,Python栈帧包含了比x64栈帧更多的信息
struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* previous frame, or NULL */
PyCodeObject *f_code; /* code segment */
PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
PyObject *f_globals; /* global symbol table (PyDictObject) */
PyObject *f_locals; /* local symbol table (any mapping) */
PyObject **f_valuestack; /* points after the last local */
PyObject *f_trace; /* Trace function */
int f_stackdepth; /* Depth of value stack */
char f_trace_lines; /* Emit per-line trace events? */
char f_trace_opcodes; /* Emit per-opcode trace events? */
/* Borrowed reference to a generator, or NULL */
PyObject *f_gen;
int f_lasti; /* Last instruction if called */
int f_lineno; /* Current line number. Only valid if non-zero */
int f_iblock; /* index in f_blockstack */
PyFrameState f_state; /* What state the frame is in */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
};
struct _frame *f_back;
:在python的执行过程中,会形成很多个栈帧,这些栈帧以链表的方式被链接起来。在x64上,栈帧间通过rsp,rbp指针联系起来,而python正是通过f_back模拟了这一过程。PyCodeObject *f_code;
:每一个python frame对应了一个PyCodeObject,在编译PyCodeObject时会计算co_stacksize的大小,即执行字段代码的栈空间大小,由于栈空间不固定,所以PyFrameObject使用变长对象头部。PyObject *f_builtins;
:维护内建变量的键值对关系。PyObject *f_globals;
:维护全局变量的键值对关系。PyObject *f_locals;
:维护局部变量的键值对关系。在python中可以使用sys._getframe()获取当前函数的栈帧对象。
>>> import sys >>> >>> >>> def func0(): ... func0_var0 = 0 ... func0_var1 = 1 ... return func1() ... >>> >>> def func1(): ... func1_var0 = 10 ... func1_var1 = 11 ... return func2() ... >>> >>> def func2(): ... func2_var0 = 20 ... func2_var1 = 21 ... return sys._getframe() ... >>> f = func0() >>> >>> print(f) <frame at 0x000001E6EBF9F650, file '<stdin>', line 4, code func2> >>> print(f.f_back) <frame at 0x000001E6EBF9F4A0, file '<stdin>', line 4, code func1> >>> print(f.f_back.f_back) <frame at 0x000001E6EBF9F2F0, file '<stdin>', line 4, code func0> >>> >>> print(f.f_locals) {'func2_var0': 20, 'func2_var1': 21} >>> print(f.f_back.f_locals) {'func1_var0': 10, 'func1_var1': 11} >>> print(f.f_back.f_back.f_locals) {'func0_var0': 0, 'func0_var1': 1} >>>
_PyEval_EvalFrameDefault
在ceval.c中使用_PyEval_EvalFrameDefault函数执行栈帧。该函数约3000行,但大体逻辑简单,使用
for(;;)
循环,判断指令码,执行指令码。PyObject* _Py_HOT_FUNCTION _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag) { ... ... PyObject **stack_pointer; /* Next free slot in value stack */ const _Py_CODEUNIT *next_instr; int opcode; /* Current opcode */ int oparg; /* Current opcode argument, if any */ PyObject **fastlocals, **freevars; PyObject *retval = NULL; /* Return value */ _Py_atomic_int * const eval_breaker = &tstate->interp->ceval.eval_breaker; PyCodeObject *co; const _Py_CODEUNIT *first_instr; PyObject *names; PyObject *consts; _PyOpcache *co_opcache; ... ... // 逐条取出字节码来执行 for (;;) { assert(stack_pointer >= f->f_valuestack); assert(STACK_LEVEL() <= co->co_stacksize); assert(!_PyErr_Occurred(tstate)); if (_Py_atomic_load_relaxed(eval_breaker)) { opcode = _Py_OPCODE(*next_instr); ... } ... ... //判断该指令属于什么操作,然后执行相应的逻辑 switch (opcode) { // 加载常量 case LOAD_CONST: ... break; // 加载名字 case LOAD_NAME: ... break; ... } } }
PyCodeObject对象里面的co_code域则保存着字节码指令和字节码指令参数,python执行字节码指令序列的过程就是从头到尾遍历整个co_code、依次执行字节码指令的过程。co_code在实质上是一个PyBytesObject,底层维护着一个char数组,在Include/opcode.h中定义了所有的字节码指令,python同时使用以下三个变量维护字节码遍历的过程。
first_instr
:永远指向字节码指令序列的开始位置next_instr
:永远指向下一条待执行的字节码指令的位置f_lasti
:指向上一条已经执行过的字节码指令的位置,f_lasti
在PyCodeObject中
- Python的字节码有的是带有参数的,有的是没有参数的,而判断字节码是否带有参数是通过HAS_AGR这个宏来实现的。对于不同的字节码指令,由于存在是否需要指令参数的区别,所以next_instr的位移可以是不同的,但无论如何,next_instr总是指向python下一条要执行的字节码。
- Python在获得了一条字节码指令和其需要的参数指令之后,会对字节码进行switch判断,根据判断的结果选择不同的case语句,每一条指令都会对应一个case语句。在case语句中,就是Python对字节码指令的实现。因为指令有121个,所以这个switch语句非常的长,比如:LOAD_CONST、LOAD_NAME、YIELD_FROM等等,而每一个指令都要对应一个case语句。
Comments | NOTHING