編者按】下面博文將帶你創建一個字節碼級別的追蹤API以追蹤Python的一些內部機制,比如類似YIELDVALUE、YIELDFROM操作碼的實現,推式構造列表(List Comprehensions)、生成器表達式(generator expressions)以及其他一些有趣Python的編譯。
關于譯者:趙斌, OneAPM工程師,常年使用 Python/Perl 腳本,從事 DevOP、測試開發相關的開發工作。業余熱愛看書,喜歡 MOOC。
以下為譯文
近我在學習 Python 的運行模型。我對 Python 的一些內部機制很是好奇,比如 Python 是怎么實現類似 YIELDVALUE、YIELDFROM 這樣的操作碼的;對于 遞推式構造列表(List Comprehensions)、生成器表達式(generator expressions)以及其他一些有趣的 Python 特性是怎么編譯的;從字節碼的層面來看,當異常拋出的時候都發生了什么事情。翻閱 CPython 的代碼對于解答這些問題當然是很有幫助的,但我仍然覺得以這樣的方式來做的話對于理解字節碼的執行和堆棧的變化還是缺少點什么。GDB 是個好選擇,但是我懶,而且只想使用一些比較高階的接口寫點 Python 代碼來完成這件事。
所以呢,我的目標就是創建一個字節碼級別的追蹤 API,類似 sys.setrace 所提供的那樣,但相對而言會有更好的粒度。這充分鍛煉了我編寫 Python 實現的 C 代碼的編碼能力。我們所需要的有如下幾項,在這篇文章中所用的 Python 版本為 3.5。
一個新的 Cpython 解釋器操作碼
一種將操作碼注入到 Python 字節碼的方法
一些用于處理操作碼的 Python 代碼
這個新的操作碼 DEBUG_OP 是我次嘗試寫 CPython 實現的 C 代碼,我將盡可能的讓它保持簡單。 我們想要達成的目的是,當我們的操作碼被執行的時候我能有一種方式來調用一些 Python 代碼。同時,我們也想能夠追蹤一些與執行上下文有關的數據。我們的操作碼會把這些信息當作參數傳遞給我們的回調函數。通過操作碼能辨識出的有用信息如下:
堆棧的內容
執行 DEBUG_OP 的幀對象信息
所以呢,我們的操作碼需要做的事情是:
找到回調函數
創建一個包含堆棧內容的列表
調用回調函數,并將包含堆棧內容的列表和當前幀作為參數傳遞給它
聽起來挺簡單的,現在開始動手吧!聲明:下面所有的解釋說明和代碼是經過了大量段錯誤調試之后總結得到的結論。首先要做的是給操作碼定義一個名字和相應的值,因此我們需要在Include/opcode.h中添加代碼。
[py] view plaincopy
/** 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__。
我們可以看到這兩標識符被使用在操作碼SETUP_WITH中:
[py] view plaincopy
/** 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的定義
[py] view plaincopy
/** 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,所以我們操作碼的查找部分的代碼就是長這樣滴。
[py] view plaincopy
/** 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();
}
為了方便理解,對這一段代碼做一些說明:
f是當前的幀,f->f_globals是它的全局區域
如果我們沒有找到op_target,我們將會檢查這個異常是不是KeyError
goto error;是一種在 main loop 中拋出異常的方法
PyErr_Clear()抑制了當前異常的拋出,而DISPATCH()觸發了下一個操作碼的執行
下一步就是收集我們想要的堆棧信息。
[py] view plaincopy
/** 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 。
[py] view plaincopy
/** 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的草稿了:
[py] view plaincopy
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 中做如下修改。
[py] view plaincopy
/** 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 字節碼中注入新的操作碼:
使用 peephole optimizer, Quarkslab就是這么干的
在生成字節碼的代碼中動些手腳
在運行時直接修改函數的字節碼(這就是我們將要干的事兒)
為了創造出一個新操作碼,有了上面的那一堆 C 代碼就夠了。現在讓我們回到原點,開始理解奇怪甚至神奇的 Python!
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。