Pythonプログラムの実行手順を下から簡単に分析する
だから、私の目標はsysのようなバイトコードレベルの追跡APIを作成することです.setraceが提供するように、相対的により良い粒度があります.これは私がPythonで実現したCコードの符号化能力を十分に鍛えた.この記事で使用するPythonバージョンは3.5です.
この新しい操作コードDEBUG_OPはCPython実装のCコードを書くのは初めてで、できるだけ簡単にします.私たちが達成したい目的は、私たちのオペレーティングコードが実行されると、Pythonコードを呼び出す方法があります.また、実行コンテキストに関連するデータも追跡したいと考えています.私たちのオペレーティングコードは、これらの情報をパラメータとして私たちのコールバック関数に渡します.操作コードで認識できる有用な情報は以下の通りである.
簡単そうですね.今から始めましょう.宣言:次のすべての説明とコードは、大量のセグメントエラーデバッグを経てまとめられた結論です.まず、オペレーティングコードに名前と対応する値を定義するので、Include/opcodeが必要です.hにコードを追加します.
/** 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を見てみましょう.
この2つの識別子がオペレーティングコードSETUPに使用されていることがわかります.WITH中:
/** 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定義
/** 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なので、コードの検索部分を操作するコードは長いです.
/** 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();
}
理解を容易にするために、このコードについて説明します.
次のステップは、私たちが望んでいるスタック情報を収集することです.
/** 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 .
/** 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の下書き
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では以下のように修正する.
/** 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バイトコードに新しいオペレーティングコードを注入する方法はたくさんあります.
私たちがしなければならないことは、
code objectに関するチラシ
code objectを聞いたことがない場合は、簡単な紹介ネットワークにもいくつかの関連ドキュメントがあります.Ctrl+Fでcode objectを直接検索することができます.
もう一つ注意しなければならないのは、この文章が指す環境でcode objectは可変ではないことです.
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__
at 0x7f481fd88390, file "", line 1>
>>> x.__code__.co_name
''
>>> x.__code__.co_name = 'truc'
Traceback (most recent call last):
File "", line 1, in
AttributeError: readonly attribute
>>> x.__code__.co_consts = ('truc',)
Traceback (most recent call last):
File "", line 1, in
AttributeError: readonly attribute
, 。
:
- dis
- dis.BytecodePython 3.4 ,
- code object
dis.Bytecode code object 、 。
# 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。
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 。
>>> x = lambda y : 2
>>> m = MutableCodeObject(x.__code__)
>>> m
>>> m.co_consts
[None, 2]
>>> m.co_consts[1] = '3'
>>> m.co_name = 'truc'
>>> m.get_code()
", line 1>
DEBUG_OP , 。 :
from new_code import MutableCodeObject
def op_target(*args):
print("WOOT")
print("op_target called with args ".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 )>
WOOT
op_target called with args )>
! new_nop_code.co_stacksize += 3
- co_stacksize code object
- DEBUG_OP ,
Python !
, Pyhton so easy。 , , ( )。 , dis.Bytecode, 。
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
, ,
,
:
# Helper
def bytecode_to_string(bytecode):
if bytecode.arg is not None:
return struct.pack(" 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)
:
>>> 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)
[]
[]
[]
[10, ]
[range(0, 10)]
[]
[0, ]
[]
[2, ]
[0, 2, ]
[False, ]
[]
[1, ]
[]
[2, ]
[1, 2, ]
[False, ]
[]
[2, ]
[]
[2, ]
[2, 2, ]
[True, ]
[]
[]
[None]
! Python 。 。 。
Python
, 。 op_target ( , )。
, :
- f_code code object
- f_lasti (code object )
DEBUG_OP , 。
:
- co_code
- op_debug
, 。 auto-follow-called-functions 。
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 。
:
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("
".join(self.report))
。 , 。
- 1
- 2
(List Comprehensions) 。
- 3
- 4
Python , main loop,Python C 、Python 。 Python , 、 。
。 , 。 , 。