- 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的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我是 C 语言新手,我编写了这个 C 程序,让用户输入一年中的某一天,作为返回,程序将输出月份以及该月的哪一天。该程序运行良好,但我现在想简化该程序。我知道我需要一个循环,但我不知道如何去做。这是程序
我一直在努力找出我的代码有什么问题。这个想法是创建一个小的画图程序,并有红色、绿色、蓝色和清除按钮。我有我能想到的一切让它工作,但无法弄清楚代码有什么问题。程序打开,然后立即关闭。 import ja
我想安装screen,但是接下来我应该做什么? $ brew search screen imgur-screenshot screen
我有一个在服务器端工作的 UDP 套接字应用程序。为了测试服务器端,我编写了一个简单的 python 客户端程序,它发送消息“hello world how are you”。服务器随后应接收消息,将
我有一个 shell 脚本,它运行一个 Python 程序来预处理一些数据,然后运行一个 R 程序来执行一些长时间运行的任务。我正在学习使用 Docker 并且我一直在运行 FROM r-base:l
在 Linux 中。我有一个 c 程序,它读取一个 2048 字节的文本文件作为输入。我想从 Python 脚本启动 c 程序。我希望 Python 脚本将文本字符串作为参数传递给 c 程序,而不是将
对于一个类,我被要求编写一个 VHDL 程序,该程序接受两个整数输入 A 和 B,并用 A+B 替换 A,用 A-B 替换 B。我编写了以下程序和测试平台。它完成了实现和行为语法检查,但它不会模拟。尽
module Algorithm where import System.Random import Data.Maybe import Data.List type Atom = String ty
我想找到两个以上数字的最小公倍数 求给定N个数的最小公倍数的C++程序 最佳答案 int lcm(int a, int b) { return (a/gcd(a,b))*b; } 对于gcd,请查看
这个程序有错误。谁能解决这个问题? Error is :TempRecord already defines a member called 'this' with the same paramete
当我运行下面的程序时,我在 str1 和 str2 中得到了垃圾值。所以 #include #include #include using namespace std; int main() {
这是我的作业: 一对刚出生的兔子(一公一母)被放在田里。兔子在一个月大时可以交配,因此在第二个月的月底,每对兔子都会生出两对新兔子,然后死去。 注:在第0个月,有0对兔子。第 1 个月,有 1 对兔子
我编写了一个程序,通过对字母使用 switch 命令将十进制字符串转换为十六进制,但是如果我使用 char,该程序无法正常工作!没有 switch 我无法处理 9 以上的数字。我希望你能理解我,因为我
我是 C++ 新手(虽然我有一些 C 语言经验)和 MySQL,我正在尝试制作一个从 MySQL 读取数据库的程序,我一直在关注这个 tutorial但当我尝试“构建”解决方案时出现错误。 (我正在使
仍然是一个初学者,只是尝试使用 swift 中的一些基本函数。 有人能告诉我这段代码有什么问题吗? import UIKit var guessInt: Int var randomNum = arc
我正在用 C++11 编写一个函数,它采用 constant1 + constant2 形式的表达式并将它们折叠起来。 constant1 和 constant2 存储在 std::string 中,
我用 C++ 编写了这段代码,使用运算符重载对 2 个矩阵进行加法和乘法运算。当我执行代码时,它会在第 57 行和第 59 行产生错误,非法结构操作(两行都出现相同的错误)。请解释我的错误。提前致谢:
我是 C++ 的初学者,我想编写一个简单的程序来交换字符串中的两个字符。 例如;我们输入这个字符串:“EXAMPLE”,我们给它交换这两个字符:“E”和“A”,输出应该类似于“AXEMPLA”。 我在
我需要以下代码的帮助: 声明 3 个 double 类型变量,每个代表三角形的三个边中的一个。 提示用户为第一面输入一个值,然后 将用户的输入设置为您创建的代表三角形第一条边的变量。 将最后 2 个步
我是新来的,如果问题不好请见谅 任务:将给定矩阵旋转180度 输入: 1 4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 输出: 16 15 14 13 12 11
我是一名优秀的程序员,十分优秀!