Mysql实例从底层简析Python程序的执行过程
《Mysql实例从底层简析Python程序的执行过程》要点: 最近我在学习 Python 的运行模型.我对 Python 的一些内部机制很是好奇,比如 Python 是怎么实现类似 YIELDVALUE、YIELDFROM 这样的操作码的;对于 递推式构造列表(List Comprehensions)、生成器表达式(generator expressions)以及其他一些有趣的 Python 特性是怎么编译的;从字节码的层面来看,当异常抛出的时候都发生了什么事情.翻阅 CPython 的代码对于解答这些问题当然是很有赞助的,但我仍然觉得以这样的方式来做的话对于理解字节码的执行和堆栈的变化还是缺少点什么.GDB 是个好选择,但是我懒,而且只想使用一些比较高阶的接口写点 Python 代码来完成这件事.MYSQL数据库 所以呢,我的目标就是创建一个字节码级别的追踪 API,类似 sys.setrace 所提供的那样,但相对而言会有更好的粒度.这充分熬炼了我编写 Python 实现的 C 代码的编码能力.我们所需要的有如下几项,在这篇文章中所用的 Python 版本为 3.5.MYSQL数据库
一个新的 Cpython 操作码 这个新的操作码 DEBUG_OP 是我第一次尝试写 CPython 实现的 C 代码,我将尽可能的让它坚持简单. 我们想要达成的目的是,当我们的操作码被执行的时候我能有一种方式来调用一些 Python 代码.同时,我们也想能够追踪一些与执行上下文有关的数据.我们的操作码会把这些信息当作参数传递给我们的回调函数.通过操作码能辨识出的有用信息如下:MYSQL数据库
所以呢,我们的操作码必要做的事情是:MYSQL数据库
听起来挺简单的,现在开始动手吧!声明:下面所有的解释说明和代码是经过了大量段错误调试之后总结得到的结论.首先要做的是给操作码定义一个名字和相应的值,因此我们需要在 Include/opcode.h中添加代码.MYSQL数据库 /** My own comments begin by '**' **/ /** From: Includes/opcode.h **/ /* Instruction opcodes for compiled code */ /** We just have to define our opcode with a free value 0 was the first one I found **/ #define DEBUG_OP 0 #define POP_TOP 1 #define ROT_TWO 2 #define ROT_THREE 3 这部门工作就完成了,现在我们去编写操作码真正干活的代码. 在考虑如何实现DEBUG_OP之前我们需要了解的是 DEBUG_OP 提供的接口将长什么样. 拥有一个可以调用其他代码的新操作码是相当酷眩的,但是究竟它将调用哪些代码捏?这个操作码如何找到回调函数的捏?我选择了一种最简单的办法:在帧的全局区域写死函数名.那么问题就变成了,我该怎么从字典中找到一个固定的 C 字符串?为了回答这个问题我们来看看在 Python 的 main loop 中使用到的和上下文管理相关的标识符 enter 和 exit.MYSQL数据库 我们可以看到这两标识符被使用在操作码 SETUP_WITH 中:MYSQL数据库 /** From: Python/ceval.c **/ TARGET(SETUP_WITH) { _Py_IDENTIFIER(__exit__); _Py_IDENTIFIER(__enter__); PyObject *mgr = TOP(); PyObject *exit = special_lookup(mgr,&PyId___exit__),*enter; PyObject *res; 现在,看一眼宏 _Py_IDENTIFIER 定义MYSQL数据库 /** From: Include/object.h **/ /********************* String Literals ****************************************/ /* This structure helps managing static strings. The basic usage goes like this: Instead of doing r = PyObject_CallMethod(o,"foo","args",...); do _Py_IDENTIFIER(foo); ... r = _PyObject_CallMethodId(o,&PyId_foo,...); PyId_foo is a static variable,either on block level or file level. On first usage,the string "foo" is interned,and the structures are linked. On interpreter shutdown,all strings are released (through _PyUnicode_ClearStaticStrings). Alternatively,_Py_static_string allows to choose the variable name. _PyUnicode_FromId returns a borrowed reference to the interned string. _PyObject_{Get,Set,Has}AttrId are __getattr__ versions using _Py_Identifier*. */ typedef struct _Py_Identifier { struct _Py_Identifier *next; const char* string; PyObject *object; } _Py_Identifier; #define _Py_static_string_init(value) { 0,value,0 } #define _Py_static_string(varname,value) static _Py_Identifier varname = _Py_static_string_init(value) #define _Py_IDENTIFIER(varname) _Py_static_string(PyId_##varname,#varname) 嗯,注释部分已经说明得很清楚了.通过一番查找,我们发现了可以用来从字典找固定字符串的函数 _PyDict_GetItemId,所以我们操作码的查找部分的代码便是长这样滴.MYSQL数据库 /** Our callback function will be named op_target **/ PyObject *target = NULL; _Py_IDENTIFIER(op_target); target = _PyDict_GetItemId(f->f_globals,&PyId_op_target); if (target == NULL && _PyErr_OCCURRED()) { if (!PyErr_ExceptionMatches(PyExc_KeyError)) goto error; PyErr_Clear(); DISPATCH(); } 为了便利理解,对这一段代码做一些说明:MYSQL数据库
下一步就是收集我们想要的堆栈信息.MYSQL数据库 /** This code create a list with all the values on the current stack **/ PyObject *value = PyList_New(0); for (i = 1 ; i <= STACK_LEVEL(); i++) { tmp = PEEK(i); if (tmp == NULL) { tmp = Py_None; } PyList_Append(value,tmp); } 最后一步便是调用我们的回调函数!我们用 call_function 来搞定这件事,我们通过研究操作码 CALL_FUNCTION 的实现来学习怎么使用 call_function .MYSQL数据库 /** From: Python/ceval.c **/ TARGET(CALL_FUNCTION) { PyObject **sp,*res; /** stack_pointer is a local of the main loop. It's the pointer to the stacktop of our frame **/ sp = stack_pointer; res = call_function(&sp,oparg); /** call_function handles the args it consummed on the stack for us **/ stack_pointer = sp; PUSH(res); /** Standard exception handling **/ if (res == NULL) goto error; DISPATCH(); } 有了上面这些信息,我们终于可以捣鼓出一个操作码DEBUG_OP的草稿了:MYSQL数据库 TARGET(DEBUG_OP) { PyObject *value = NULL; PyObject *target = NULL; PyObject *res = NULL; PyObject **sp = NULL; PyObject *tmp; int i; _Py_IDENTIFIER(op_target); target = _PyDict_GetItemId(f->f_globals,&PyId_op_target); if (target == NULL && _PyErr_OCCURRED()) { if (!PyErr_ExceptionMatches(PyExc_KeyError)) goto error; PyErr_Clear(); DISPATCH(); } value = PyList_New(0); Py_INCREF(target); for (i = 1 ; i <= STACK_LEVEL(); i++) { tmp = PEEK(i); if (tmp == NULL) tmp = Py_None; PyList_Append(value,tmp); } PUSH(target); PUSH(value); Py_INCREF(f); PUSH(f); sp = stack_pointer; res = call_function(&sp,2); stack_pointer = sp; if (res == NULL) goto error; Py_DECREF(res); DISPATCH(); } 在编写 CPython 实现的 C 代码方面我确实没有什么经验,有可能我漏掉了些细节.如果您有什么建议还请您纠正,我期待您的反馈.MYSQL数据库 编译它,成了!MYSQL数据库 一切看起来很顺利,但是当我们尝试去使用我们定义的操作码 DEBUG_OP 的时候却失败了.自从 2008 年之后,Python 使用预先写好的 goto(你也可以从 这里获取更多的讯息).故,我们必要更新下 goto jump table,我们在 Python/opcode_targets.h 中做如下修改.MYSQL数据库 /** From: Python/opcode_targets.h **/ /** Easy change since DEBUG_OP is the opcode number 1 **/ static void *opcode_targets[256] = { //&&_unknown_opcode,&&TARGET_DEBUG_OP,&&TARGET_POP_TOP,/** ... **/ 这就完事了,我们现在就有了一个可以工作的新操作码.唯一的问题便是这货虽然存在,但是没有被人调用过.接下来,我们将DEBUG_OP注入到函数的字节码中. 有很多方式可以在 Python 字节码中注入新的操作码:MYSQL数据库
为了创造出一个新操作码,有了上面的那一堆 C 代码就够了.现在让我们回到原点,开始理解奇怪甚至神奇的 Python!MYSQL数据库 我们将要做的事儿有:MYSQL数据库
和 code object 有关的小贴士MYSQL数据库 如果你从没听说过 code object,这里有一个简单的介绍网路上也有一些相关的文档可供查阅,可以直接 Ctrl+F 查找 code objectMYSQL数据库 还有一件事情必要注意的是在这篇文章所指的环境中 code object 是不可变的:MYSQL数据库 Python 3.4.2 (default,Oct 8 2014,10:45:20) [GCC 4.9.1] on linux Type "help","copyright","credits" or "license" for more information. >>> x = lambda y : 2 >>> x.__code__ <code object <lambda> at 0x7f481fd88390,file "<stdin>",line 1> >>> x.__code__.co_name '<lambda>' >>> x.__code__.co_name = 'truc' Traceback (most recent call last): File "<stdin>",line 1,in <module> AttributeError: readonly attribute >>> x.__code__.co_consts = ('truc',) Traceback (most recent call last): File "<stdin>",in <module> AttributeError: readonly attribute 但是不用担心,我们将会找到办法绕过这个问题的. 为了修改字节码我们必要一些工具:MYSQL数据库
用 dis.Bytecode 反编译 code object 能告诉我们一些有关操作码、参数和上下文的信息.MYSQL数据库 # Python3.4 >>> import dis >>> f = lambda x: x + 3 >>> for i in dis.Bytecode(f.__code__): print (i) ... Instruction(opname='LOAD_FAST',opcode=124,arg=0,argval='x',argrepr='x',offset=0,starts_line=1,is_jump_target=False) Instruction(opname='LOAD_CONST',opcode=100,arg=1,argval=3,argrepr='3',offset=3,starts_line=None,is_jump_target=False) Instruction(opname='BINARY_ADD',opcode=23,arg=None,argval=None,argrepr='',offset=6,is_jump_target=False) Instruction(opname='RETURN_VALUE',opcode=83,offset=7,is_jump_target=False) 为了能够修改 code object,我定义了一个很小的类用来复制 code object,同时能够按我们的需求修改相应的值,然后重新生成一个新的 code object.MYSQL数据库 class MutableCodeObject(object): args_name = ("co_argcount","co_kwonlyargcount","co_nlocals","co_stacksize","co_flags","co_code","co_consts","co_names","co_varnames","co_filename","co_name","co_firstlineno","co_lnotab","co_freevars","co_cellvars") def __init__(self,initial_code): self.initial_code = initial_code for attr_name in self.args_name: attr = getattr(self.initial_code,attr_name) if isinstance(attr,tuple): attr = list(attr) setattr(self,attr_name,attr) def get_code(self): args = [] for attr_name in self.args_name: attr = getattr(self,list): attr = tuple(attr) args.append(attr) return self.initial_code.__class__(*args) 这个类用起来很方便,办理了上面提到的 code object 不可变的问题.MYSQL数据库 >>> x = lambda y : 2 >>> m = MutableCodeObject(x.__code__) >>> m <new_code.MutableCodeObject object at 0x7f3f0ea546a0> >>> m.co_consts [None,2] >>> m.co_consts[1] = '3' >>> m.co_name = 'truc' >>> m.get_code() <code object truc at 0x7f3f0ea2bc90,line 1> 测试我们的新操作码MYSQL数据库 我们现在拥有了注入 DEBUG_OP 的所有工具,让我们来验证下我们的实现是否可用.我们将我们的操作码注入到一个最简单的函数中:MYSQL数据库 from new_code import MutableCodeObject def op_target(*args): print("WOOT") print("op_target called with args <{0}>".format(args)) def nop(): pass new_nop_code = MutableCodeObject(nop.__code__) new_nop_code.co_code = b"x00" + new_nop_code.co_code[0:3] + b"x00" + new_nop_code.co_code[-1:] new_nop_code.co_stacksize += 3 nop.__code__ = new_nop_code.get_code() import dis dis.dis(nop) nop() # Don't forget that ./python is our custom Python implementing DEBUG_OP hakril@computer ~/python/CPython3.5 % ./python proof.py 8 0 <0> 1 LOAD_CONST 0 (None) 4 <0> 5 RETURN_VALUE WOOT op_target called with args <([],<frame object at 0x7fde9eaebdb0>)> WOOT op_target called with args <([None],<frame object at 0x7fde9eaebdb0>)> 看起来它成功了!有一行代码必要说明一下 new_nop_code.co_stacksize += 3MYSQL数据库
现在我们可以将我们的操作码注入到每一个 Python 函数中了! 正如我们在上面的例子中所看到的那样,重写 Pyhton 的字节码似乎 so easy.为了在每一个操作码之间注入我们的操作码,我们必要获取每一个操作码的偏移量,然后将我们的操作码注入到这些位置上(把我们操作码注入到参数上是有坏处大大滴).这些偏移量也很容易获取,使用 dis.Bytecode,就像这样.MYSQL数据库 def add_debug_op_everywhere(code_obj): # We get every instruction offset in the code object offsets = [instr.offset for instr in dis.Bytecode(code_obj)] # And insert a DEBUG_OP at every offset return insert_op_debug_list(code_obj,offsets) def insert_op_debug_list(code,offsets): # We insert the DEBUG_OP one by one for nb,off in enumerate(sorted(offsets)): # Need to ajust the offsets by the number of opcodes already inserted before # That's why we sort our offsets! code = insert_op_debug(code,off + nb) return code # Last problem: what does insert_op_debug looks like? 基于上面的例子,有人可能会想我们的 insert_op_debug 会在指定的偏移量增加一个"x00",这尼玛是个坑啊!我们第一个 DEBUG_OP 注入的例子中被注入的函数是没有任何的分支的,为了能够实现完美一个函数注入函数 insert_op_debug 我们必要考虑到存在分支操作码的情况.MYSQL数据库 Python 的分支一共有两种:MYSQL数据库 ???(1) 绝对分支:看起来是类似这样子的 Instruction_Pointer = argument(instruction)MYSQL数据库 ??? (2)相对分支:看起来是类似这样子的 Instruction_Pointer += argument(instruction)MYSQL数据库 ?????????????? 相对分支总是向前的MYSQL数据库 我们希望这些分支在我们插入操作码之后仍然能够正常工作,为此我们必要修改一些指令参数.以下是其逻辑流程:MYSQL数据库 ???(1) 对于每一个在插入偏移量之前的相对分支而言MYSQL数据库 ??????? 如果目标地址是严格年夜于我们的插入偏移量的话,将指令参数增加 1MYSQL数据库 ??????? 如果相等,则不必要增加 1 就能够在跳转操作和目标地址之间执行我们的操作码DEBUG_OPMYSQL数据库 ??????? 如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离MYSQL数据库 ???(2) 对于 code object 中的每一个绝对分支而言MYSQL数据库 ??????? 如果目标地址是严格年夜于我们的插入偏移量的话,那么不必要任何修改,理由和相对分支部分是一样的MYSQL数据库 ??????? 如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离MYSQL数据库 下面是实现:MYSQL数据库 # Helper def bytecode_to_string(bytecode): if bytecode.arg is not None: return struct.pack("<Bh",bytecode.opcode,bytecode.arg) return struct.pack("<B",bytecode.opcode) # Dummy class for bytecode_to_string class DummyInstr: def __init__(self,opcode,arg): self.opcode = opcode self.arg = arg def insert_op_debug(code,offset): opcode_jump_rel = ['FOR_ITER','JUMP_FORWARD','SETUP_LOOP','SETUP_WITH','SETUP_EXCEPT','SETUP_FINALLY'] opcode_jump_abs = ['POP_JUMP_IF_TRUE','POP_JUMP_IF_FALSE','JUMP_ABSOLUTE'] res_codestring = b"" inserted = False for instr in dis.Bytecode(code): if instr.offset == offset: res_codestring += b"x00" inserted = True if instr.opname in opcode_jump_rel and not inserted: #relative jump are always forward if offset < instr.offset + 3 + instr.arg: # inserted beetwen jump and dest: add 1 to dest (3 for size) #If equal: jump on DEBUG_OP to get info before exec instr res_codestring += bytecode_to_string(DummyInstr(instr.opcode,instr.arg + 1)) continue if instr.opname in opcode_jump_abs: if instr.arg > offset: res_codestring += bytecode_to_string(DummyInstr(instr.opcode,instr.arg + 1)) continue res_codestring += bytecode_to_string(instr) # replace_bytecode just replaces the original code co_code return replace_bytecode(code,res_codestring) 让我们看一下效果如何:MYSQL数据库 ??MYSQL数据库 >>> def lol(x): ... for i in range(10): ... if x == i: ... break >>> dis.dis(lol) 101 0 SETUP_LOOP 36 (to 39) 3 LOAD_GLOBAL 0 (range) 6 LOAD_CONST 1 (10) 9 CALL_FUNCTION 1 (1 positional,0 keyword pair) 12 GET_ITER >> 13 FOR_ITER 22 (to 38) 16 STORE_FAST 1 (i) 102 19 LOAD_FAST 0 (x) 22 LOAD_FAST 1 (i) 25 COMPARE_OP 2 (==) 28 POP_JUMP_IF_FALSE 13 103 31 BREAK_LOOP 32 JUMP_ABSOLUTE 13 35 JUMP_ABSOLUTE 13 >> 38 POP_BLOCK >> 39 LOAD_CONST 0 (None) 42 RETURN_VALUE >>> lol.__code__ = transform_code(lol.__code__,add_debug_op_everywhere,add_stacksize=3) >>> dis.dis(lol) 101 0 <0> 1 SETUP_LOOP 50 (to 54) 4 <0> 5 LOAD_GLOBAL 0 (range) 8 <0> 9 LOAD_CONST 1 (10) 12 <0> 13 CALL_FUNCTION 1 (1 positional,0 keyword pair) 16 <0> 17 GET_ITER >> 18 <0> 102 19 FOR_ITER 30 (to 52) 22 <0> 23 STORE_FAST 1 (i) 26 <0> 27 LOAD_FAST 0 (x) 30 <0> 103 31 LOAD_FAST 1 (i) 34 <0> 35 COMPARE_OP 2 (==) 38 <0> 39 POP_JUMP_IF_FALSE 18 42 <0> 43 BREAK_LOOP 44 <0> 45 JUMP_ABSOLUTE 18 48 <0> 49 JUMP_ABSOLUTE 18 >> 52 <0> 53 POP_BLOCK >> 54 <0> 55 LOAD_CONST 0 (None) 58 <0> 59 RETURN_VALUE # Setup the simplest handler EVER >>> def op_target(stack,frame): ... print (stack) # GO >>> lol(2) [] [] [<class 'range'>] [10,<class 'range'>] [range(0,10)] [<range_iterator object at 0x7f1349afab80>] [0,<range_iterator object at 0x7f1349afab80>] [<range_iterator object at 0x7f1349afab80>] [2,<range_iterator object at 0x7f1349afab80>] [0,2,<range_iterator object at 0x7f1349afab80>] [False,<range_iterator object at 0x7f1349afab80>] [<range_iterator object at 0x7f1349afab80>] [1,<range_iterator object at 0x7f1349afab80>] [1,<range_iterator object at 0x7f1349afab80>] [2,<range_iterator object at 0x7f1349afab80>] [True,<range_iterator object at 0x7f1349afab80>] [<range_iterator object at 0x7f1349afab80>] [] [None] 甚好!现在我们知道了如何获取堆栈信息和 Python 中每一个操作对应的帧信息.上面结果所展示的结果目前而言并不是很实用.在最后一部门中让我们对注入做进一步的封装. 正如您所见到的,所有的底层接口都是好用的.我们最后要做的一件事是让 op_target 更加方便使用(这部分相对而言比拟空泛一些,毕竟在我看来这不是整个项目中最有趣的部分).MYSQL数据库 首先我们来看一下帧的参数所能提供的信息,如下所示:MYSQL数据库
经过我们的处置我们可以得知 DEBUG_OP 之后要被执行的操作码,这对我们聚合数据并展示是相当有用的.MYSQL数据库 新建一个用于追踪函数内部机制的类:MYSQL数据库
一旦我们知道下一个操作,我们就可以阐发它并修改它的参数.举例来说我们可以增加一个 auto-follow-called-functions 的特性.MYSQL数据库 ??MYSQL数据库 def op_target(l,f,exc=None): if op_target.callback is not None: op_target.callback(l,exc) class Trace: def __init__(self,func): self.func = func def call(self,*args,**kwargs): self.add_func_to_trace(self.func) # Activate Trace callback for the func call op_target.callback = self.callback try: res = self.func(*args,**kwargs) except Exception as e: res = e op_target.callback = None return res def add_func_to_trace(self,f): # Is it code? is it already transformed? if not hasattr(f,"op_debug") and hasattr(f,"__code__"): f.__code__ = transform_code(f.__code__,transform=add_everywhere,add_stacksize=ADD_STACK) f.__globals__['op_target'] = op_target f.op_debug = True def do_auto_follow(self,stack,frame): # Nothing fancy: FrameAnalyser is just the wrapper that gives the next executed instruction next_instr = FrameAnalyser(frame).next_instr() if "CALL" in next_instr.opname: arg = next_instr.arg f_index = (arg & 0xff) + (2 * (arg >> 8)) called_func = stack[f_index] # If call target is not traced yet: do it if not hasattr(called_func,"op_debug"): self.add_func_to_trace(called_func) 现在我们实现一个 Trace 的子类,在这个子类中增加 callback 和 doreport 这两个办法.callback 办法将在每一个操作之后被调用.doreport 办法将我们收集到的信息打印出来.MYSQL数据库 这是一个伪函数追踪器实现:MYSQL数据库 ??MYSQL数据库 class DummyTrace(Trace): def __init__(self,func): self.func = func self.data = collections.OrderedDict() self.last_frame = None self.known_frame = [] self.report = [] def callback(self,frame,exc): if frame not in self.known_frame: self.known_frame.append(frame) self.report.append(" === Entering New Frame {0} ({1}) ===".format(frame.f_code.co_name,id(frame))) self.last_frame = frame if frame != self.last_frame: self.report.append(" === Returning to Frame {0} {1}===".format(frame.f_code.co_name,id(frame))) self.last_frame = frame self.report.append(str(stack)) instr = FrameAnalyser(frame).next_instr() offset = str(instr.offset).rjust(8) opname = str(instr.opname).ljust(20) arg = str(instr.arg).ljust(10) self.report.append("{0} {1} {2} {3}".format(offset,opname,arg,instr.argval)) self.do_auto_follow(stack,frame) def do_report(self): print("n".join(self.report)) 这里有一些实现的例子和使用办法.格式有些不方便观看,毕竟我并不擅长于搞这种对用户友好的报告的事儿.MYSQL数据库
递推式构造列表(List Comprehensions)的追踪示例.MYSQL数据库
总结MYSQL数据库 这个小项目是一个了解 Python 底层的良好途径,包含解释器的 main loop,Python 实现的 C 代码编程、Python 字节码.通过这个小工具我们可以看到 Python 一些有趣构造函数的字节码行为,例如生成器、上下文管理和递推式构造列表.MYSQL数据库 这里是这个小项目的完整代码.更进一步的,我们还可以做的是修改我们所追踪的函数的堆栈.我虽然不确定这个是否有用,然则可以肯定是这一过程是相当有趣的. 编程之家PHP培训学院每天发布《Mysql实例从底层简析Python程序的执行过程》等实战技能,PHP、MYSQL、LINUX、APP、JS,CSS全面培养人才。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |