- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章从底层简析Python程序的执行过程由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
最近我在学习 Python 的运行模型。我对 Python 的一些内部机制很是好奇,比如 Python 是怎么实现类似 YIELDVALUE、YIELDFROM 这样的操作码的;对于 递推式构造列表(List Comprehensions)、生成器表达式(generator expressions)以及其他一些有趣的 Python 特性是怎么编译的;从字节码的层面来看,当异常抛出的时候都发生了什么事情。翻阅 CPython 的代码对于解答这些问题当然是很有帮助的,但我仍然觉得以这样的方式来做的话对于理解字节码的执行和堆栈的变化还是缺少点什么。GDB 是个好选择,但是我懒,而且只想使用一些比较高阶的接口写点 Python 代码来完成这件事.
所以呢,我的目标就是创建一个字节码级别的追踪 API,类似 sys.setrace 所提供的那样,但相对而言会有更好的粒度。这充分锻炼了我编写 Python 实现的 C 代码的编码能力。我们所需要的有如下几项,在这篇文章中所用的 Python 版本为 3.5.
一个新的 Cpython 操作码 新操作码:DEBUG_OP 。
这个新的操作码 DEBUG_OP 是我第一次尝试写 CPython 实现的 C 代码,我将尽可能的让它保持简单。 我们想要达成的目的是,当我们的操作码被执行的时候我能有一种方式来调用一些 Python 代码。同时,我们也想能够追踪一些与执行上下文有关的数据。我们的操作码会把这些信息当作参数传递给我们的回调函数。通过操作码能辨识出的有用信息如下:
所以呢,我们的操作码需要做的事情是:
听起来挺简单的,现在开始动手吧!声明:下面所有的解释说明和代码是经过了大量段错误调试之后总结得到的结论。首先要做的是给操作码定义一个名字和相应的值,因此我们需要在 Include/opcode.h中添加代码.
1
2
3
4
5
6
7
8
9
10
11
12
|
/
*
*
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之前我们需要了解的是 DEBUG_OP 提供的接口将长什么样。 拥有一个可以调用其他代码的新操作码是相当酷眩的,但是究竟它将调用哪些代码捏?这个操作码如何找到回调函数的捏?我选择了一种最简单的方法:在帧的全局区域写死函数名。那么问题就变成了,我该怎么从字典中找到一个固定的 C 字符串?为了回答这个问题我们来看看在 Python 的 main loop 中使用到的和上下文管理相关的标识符 enter 和 exit.
我们可以看到这两标识符被使用在操作码 SETUP_WITH 中:
1
2
3
4
5
6
7
|
/
*
*
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 定义 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
/
*
*
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,
"args"
, ...);
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,所以我们操作码的查找部分的代码就是长这样滴.
1
2
3
4
5
6
7
8
9
10
|
/
*
*
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();
}
|
为了方便理解,对这一段代码做一些说明:
下一步就是收集我们想要的堆栈信息.
1
2
3
4
5
6
7
8
9
|
/
*
*
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 .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/
*
*
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的草稿了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
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 代码方面我确实没有什么经验,有可能我漏掉了些细节。如果您有什么建议还请您纠正,我期待您的反馈.
编译它,成了! 。
一切看起来很顺利,但是当我们尝试去使用我们定义的操作码 DEBUG_OP 的时候却失败了。自从 2008 年之后,Python 使用预先写好的 goto(你也可以从 这里获取更多的讯息)。故,我们需要更新下 goto jump table,我们在 Python/opcode_targets.h 中做如下修改.
1
2
3
4
5
6
7
|
/
*
*
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 字节码中注入操作码 DEBUG_OP 。
有很多方式可以在 Python 字节码中注入新的操作码:
为了创造出一个新操作码,有了上面的那一堆 C 代码就够了。现在让我们回到原点,开始理解奇怪甚至神奇的 Python! 。
我们将要做的事儿有:
和 code object 有关的小贴士 。
如果你从没听说过 code object,这里有一个简单的介绍网路上也有一些相关的文档可供查阅,可以直接 Ctrl+F 查找 code object 。
还有一件事情需要注意的是在这篇文章所指的环境中 code object 是不可变的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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>"
, line
1
,
in
<module>
AttributeError: readonly attribute
|
但是不用担心,我们将会找到方法绕过这个问题的。 使用的工具 。
为了修改字节码我们需要一些工具:
用 dis.Bytecode 反编译 code object 能告诉我们一些有关操作码、参数和上下文的信息.
1
2
3
4
5
6
7
8
9
|
# 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
, starts_line
=
None
, is_jump_target
=
False
)
Instruction(opname
=
'RETURN_VALUE'
, opcode
=
83
, arg
=
None
, argval
=
None
, argrepr
=
'', offset
=
7
, starts_line
=
None
, is_jump_target
=
False
)
|
为了能够修改 code object,我定义了一个很小的类用来复制 code object,同时能够按我们的需求修改相应的值,然后重新生成一个新的 code object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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
, attr_name)
if
isinstance
(attr,
list
):
attr
=
tuple
(attr)
args.append(attr)
return
self
.initial_code.__class__(
*
args)
|
这个类用起来很方便,解决了上面提到的 code object 不可变的问题.
1
2
3
4
5
6
7
8
9
10
|
>>> 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
,
file
"<stdin>"
, line
1
>
|
测试我们的新操作码 。
我们现在拥有了注入 DEBUG_OP 的所有工具,让我们来验证下我们的实现是否可用。我们将我们的操作码注入到一个最简单的函数中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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 += 3 。
现在我们可以将我们的操作码注入到每一个 Python 函数中了! 重写字节码 。
正如我们在上面的例子中所看到的那样,重写 Pyhton 的字节码似乎 so easy。为了在每一个操作码之间注入我们的操作码,我们需要获取每一个操作码的偏移量,然后将我们的操作码注入到这些位置上(把我们操作码注入到参数上是有坏处大大滴)。这些偏移量也很容易获取,使用 dis.Bytecode,就像这样.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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 我们需要考虑到存在分支操作码的情况.
Python 的分支一共有两种:
(1) 绝对分支:看起来是类似这样子的 Instruction_Pointer = argument(instruction) 。
(2)相对分支:看起来是类似这样子的 Instruction_Pointer += argument(instruction) 。
相对分支总是向前的 。
我们希望这些分支在我们插入操作码之后仍然能够正常工作,为此我们需要修改一些指令参数。以下是其逻辑流程:
(1) 对于每一个在插入偏移量之前的相对分支而言 。
如果目标地址是严格大于我们的插入偏移量的话,将指令参数增加 1 。
如果相等,则不需要增加 1 就能够在跳转操作和目标地址之间执行我们的操作码DEBUG_OP 。
如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离 。
(2) 对于 code object 中的每一个绝对分支而言 。
如果目标地址是严格大于我们的插入偏移量的话,将指令参数增加 1 。
如果相等,那么不需要任何修改,理由和相对分支部分是一样的 。
如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离 。
下面是实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
# 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)
|
让我们看一下效果如何:
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
>>>
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
>]
[<range_iterator
object
at
0x7f1349afab80
>]
[
2
, <range_iterator
object
at
0x7f1349afab80
>]
[
1
,
2
, <range_iterator
object
at
0x7f1349afab80
>]
[
False
, <range_iterator
object
at
0x7f1349afab80
>]
[<range_iterator
object
at
0x7f1349afab80
>]
[
2
, <range_iterator
object
at
0x7f1349afab80
>]
[<range_iterator
object
at
0x7f1349afab80
>]
[
2
, <range_iterator
object
at
0x7f1349afab80
>]
[
2
,
2
, <range_iterator
object
at
0x7f1349afab80
>]
[
True
, <range_iterator
object
at
0x7f1349afab80
>]
[<range_iterator
object
at
0x7f1349afab80
>]
[]
[
None
]
|
甚好!现在我们知道了如何获取堆栈信息和 Python 中每一个操作对应的帧信息。上面结果所展示的结果目前而言并不是很实用。在最后一部分中让我们对注入做进一步的封装。 增加 Python 封装 。
正如您所见到的,所有的底层接口都是好用的。我们最后要做的一件事是让 op_target 更加方便使用(这部分相对而言比较空泛一些,毕竟在我看来这不是整个项目中最有趣的部分).
首先我们来看一下帧的参数所能提供的信息,如下所示:
经过我们的处理我们可以得知 DEBUG_OP 之后要被执行的操作码,这对我们聚合数据并展示是相当有用的.
新建一个用于追踪函数内部机制的类:
一旦我们知道下一个操作,我们就可以分析它并修改它的参数。举例来说我们可以增加一个 auto-follow-called-functions 的特性.
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
def
op_target(l, f, exc
=
None
):
if
op_target.callback
is
not
None
:
op_target.callback(l, f, 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 方法将我们收集到的信息打印出来.
这是一个伪函数追踪器实现:
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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
, stack, 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))
|
这里有一些实现的例子和使用方法。格式有些不方便观看,毕竟我并不擅长于搞这种对用户友好的报告的事儿.
递推式构造列表(List Comprehensions)的追踪示例.
总结 。
这个小项目是一个了解 Python 底层的良好途径,包括解释器的 main loop,Python 实现的 C 代码编程、Python 字节码。通过这个小工具我们可以看到 Python 一些有趣构造函数的字节码行为,例如生成器、上下文管理和递推式构造列表.
这里是这个小项目的完整代码。更进一步的,我们还可以做的是修改我们所追踪的函数的堆栈。我虽然不确定这个是否有用,但是可以肯定是这一过程是相当有趣的.
最后此篇关于从底层简析Python程序的执行过程的文章就讲到这里了,如果你想了解更多关于从底层简析Python程序的执行过程的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
如果我声明了类似的类型 type test(NSIZE) integer, len :: NSIZE real :: dummy(NSIZE) contains procedure,
我知道这是一个不太可能的事情,但是由于“选项私有(private)模块”的限制,甚至更糟糕的“私有(private)子/函数”的限制,有谁知道是否有一种方法可以从 Excel 应用程序隐藏 VBA 过
我有两个表,property 和 component。 component.id_property = property.id。 我正在尝试创建一个过程,该过程对所选属性的组件进行计数,如果所选属性没
我有一份报告,它是在 SSRS 2005 中开发的,我正在使用存储过程从数据库中获取结果。报告输出的结果非常简单,如下图所示。 如果假设我正在寻找不同的成员 例如:- MemberID c108 c
我需要一个通用函数/过程,该函数/过程将根据提供的数据计算出我的淡入淡出时间和值,如下所示: 我将字节值保存在字节数组中:这些是起始值。然后,我在其他数组中存储了一些值:这些将是新值。然后我有时间要提
我想在界面的多个按钮上创建相同的操作。是否只能通过创建单独的操作监听器方法并调用执行操作的方法才可行,还是还有其他方法?是否可以将按钮放在一个组中并执行以下操作:- groupButton.setOn
我有以下情况: procedure Test; begin repeat TryAgain := FALSE; try // Code // Code if this an
我正在尝试执行以下操作;假设我在 Oracle 中创建了一个对象类型 create type test as object( name varchar2(12), member procedure p
问题: 如果可能的话,如何声明一个用于任何类型参数的函数 T其中 T 的唯一约束是它被定义为 1D array如 type T is array ( integer range <> ) of a_r
我正在尝试创建这个 mysql 过程来制作一个包含今年所有日期和所有时间的表(以一小时为间隔。) CREATE TABLE FECHAS ( created_at datetime ); CREA
所以, 我在这里面临一个问题,这让我发疯,我认为这是一个愚蠢的错误,所以我不是 MySQL 的新手,但它并不像我想象的那样工作。 尝试将此语句部署到 MySQL 后,我收到此错误: ERROR 106
我有一个架构,其中包含星球大战中的人物列表、他们出现的电影、他们访问的行星等。这是架构: CREATE DATABASE IF NOT EXISTS `starwarsFINAL` /*!40100
我一直在为一家慈善机构创建一款应用程序,允许家庭在节日期间注册接收礼物。数据库组织有多个表。下面列出了这些表(及其架构/创建语句): CREATE TABLE IF NOT EXISTS ValidD
正如上面标题所解释的,我正在尝试编写一个sql函数来按日期删除表而不删除系统表。我在此消息下方放置了一张图片,以便直观地解释我的问题。任何帮助将不胜感激!感谢您的时间! 最佳答案 您可以通过查询INF
DELIMITER $$ CREATE PROCEDURE INSERT_NONE_HISTORY_CHECKBOX() BEGIN DECLARE note_id bigint(20); F
是否可以编写一个存储过程或触发器,在特定时间在数据库内部自动执行,而无需来自应用程序的任何调用?如果是,那么任何人都可以给我一个例子或链接到一些我可以阅读如何做到这一点的资源。 最佳答案 查看 pgA
我需要创建一个过程:1)从表中的字段中选择一些文本并将其存储在变量中2) 更新相同的记录字段,仅添加 yyyymmdd 格式的日期以及过程中的附加文本输入...类似这样的... delimiter /
好的,这就是我想做的: 如果条目已存在(例如基于字段name),则只需返回其id 如果没有,请添加 这是我迄今为止所管理的(对于“如果不存在,则创建它”部分): INSERT INTO `object
以下是我编写的程序,用于找出每位客户每天购买的前 10 件商品。 这是我尝试过的第一个 PL/SQL 操作。它没有达到我预期的效果。 我使用的逻辑是接受开始日期、结束日期以及我对每个客户感兴趣的前“x
我正在尝试在MySQL中创建一个过程那insert week s(当年)发送至我的 week table 。但存在一个问题,因为在为下一行添加第一行后,我收到错误: number column can
我是一名优秀的程序员,十分优秀!