從底層帶你理解Python中的一些內(nèi)部機(jī)制

下面博文將帶你創(chuàng)建一個(gè)字節(jié)碼級(jí)別的追蹤API以追蹤Python的一些內(nèi)部機(jī)制尿扯,比如類似YIELDVALUE繁成、YIELDFROM操作碼的實(shí)現(xiàn)焚辅,推式構(gòu)造列表(List Comprehensions)凄贩、生成器表達(dá)式(generator expressions)以及其他一些有趣Python的編譯誓军。

以下為譯文

最近我在學(xué)習(xí) Python 的運(yùn)行模型。我對(duì) Python 的一些內(nèi)部機(jī)制很是好奇疲扎,比如 Python 是怎么實(shí)現(xiàn)類似 YIELDVALUE昵时、YIELDFROM 這樣的操作碼的;對(duì)于 遞推式構(gòu)造列表(List Comprehensions)椒丧、生成器表達(dá)式(generator expressions)以及其他一些有趣的 Python 特性是怎么編譯的壹甥;從字節(jié)碼的層面來(lái)看,當(dāng)異常拋出的時(shí)候都發(fā)生了什么事情瓜挽。翻閱 CPython 的代碼對(duì)于解答這些問(wèn)題當(dāng)然是很有幫助的盹廷,但我仍然覺(jué)得以這樣的方式來(lái)做的話對(duì)于理解字節(jié)碼的執(zhí)行和堆棧的變化還是缺少點(diǎn)什么征绸。GDB 是個(gè)好選擇久橙,但是我懶俄占,而且只想使用一些比較高階的接口寫(xiě)點(diǎn) Python 代碼來(lái)完成這件事。

所以呢淆衷,我的目標(biāo)就是創(chuàng)建一個(gè)字節(jié)碼級(jí)別的追蹤 API缸榄,類似 sys.setrace 所提供的那樣,但相對(duì)而言會(huì)有更好的粒度祝拯。這充分鍛煉了我編寫(xiě) Python 實(shí)現(xiàn)的 C 代碼的編碼能力甚带。我們所需要的有如下幾項(xiàng),在這篇文章中所用的 Python 版本為 3.5佳头。

  • 一個(gè)新的 Cpython 解釋器操作碼
  • 一種將操作碼注入到 Python 字節(jié)碼的方法
  • 一些用于處理操作碼的 Python 代碼

一個(gè)新的 Cpython 操作碼

新操作碼:DEBUG_OP

這個(gè)新的操作碼 DEBUG_OP 是我第一次嘗試寫(xiě) CPython 實(shí)現(xiàn)的 C 代碼鹰贵,我將盡可能的讓它保持簡(jiǎn)單。 我們想要達(dá)成的目的是康嘉,當(dāng)我們的操作碼被執(zhí)行的時(shí)候我能有一種方式來(lái)調(diào)用一些 Python 代碼碉输。同時(shí),我們也想能夠追蹤一些與執(zhí)行上下文有關(guān)的數(shù)據(jù)亭珍。我們的操作碼會(huì)把這些信息當(dāng)作參數(shù)傳遞給我們的回調(diào)函數(shù)敷钾。通過(guò)操作碼能辨識(shí)出的有用信息如下:

  • 堆棧的內(nèi)容
  • 執(zhí)行 DEBUG_OP 的幀對(duì)象信息

所以呢,我們的操作碼需要做的事情是:

  • 找到回調(diào)函數(shù)
  • 創(chuàng)建一個(gè)包含堆棧內(nèi)容的列表
  • 調(diào)用回調(diào)函數(shù)肄梨,并將包含堆棧內(nèi)容的列表和當(dāng)前幀作為參數(shù)傳遞給它

聽(tīng)起來(lái)挺簡(jiǎn)單的阻荒,現(xiàn)在開(kāi)始動(dòng)手吧!聲明:下面所有的解釋說(shuō)明和代碼是經(jīng)過(guò)了大量段錯(cuò)誤調(diào)試之后總結(jié)得到的結(jié)論众羡。首先要做的是給操作碼定義一個(gè)名字和相應(yīng)的值侨赡,因此我們需要在Include/opcode.h中添加代碼。

從底層帶你理解Python中的一些內(nèi)部機(jī)制

這部分工作就完成了粱侣,現(xiàn)在我們?nèi)ゾ帉?xiě)操作碼真正干活的代碼辆毡。

實(shí)現(xiàn) DEBUG_OP

在考慮如何實(shí)現(xiàn)DEBUG_OP之前我們需要了解的是DEBUG_OP提供的接口將長(zhǎng)什么樣。 擁有一個(gè)可以調(diào)用其他代碼的新操作碼是相當(dāng)酷眩的甜害,但是究竟它將調(diào)用哪些代碼捏舶掖?這個(gè)操作碼如何找到回調(diào)函數(shù)的捏?我選擇了一種最簡(jiǎn)單的方法:在幀的全局區(qū)域?qū)懰篮瘮?shù)名尔店。那么問(wèn)題就變成了眨攘,我該怎么從字典中找到一個(gè)固定的 C 字符串?為了回答這個(gè)問(wèn)題我們來(lái)看看在 Python 的 main loop 中使用到的和上下文管理相關(guān)的標(biāo)識(shí)符enterexit嚣州。

我們可以看到這兩標(biāo)識(shí)符被使用在操作碼SETUP_WITH中:

從底層帶你理解Python中的一些內(nèi)部機(jī)制

現(xiàn)在鲫售,看一眼宏_Py_IDENTIFIER的定義

從底層帶你理解Python中的一些內(nèi)部機(jī)制

嗯,注釋部分已經(jīng)說(shuō)明得很清楚了该肴。通過(guò)一番查找情竹,我們發(fā)現(xiàn)了可以用來(lái)從字典找固定字符串的函數(shù)_PyDict_GetItemId,所以我們操作碼的查找部分的代碼就是長(zhǎng)這樣滴匀哄。

從底層帶你理解Python中的一些內(nèi)部機(jī)制

為了方便理解秦效,對(duì)這一段代碼做一些說(shuō)明:

  • f是當(dāng)前的幀雏蛮,f->f_globals是它的全局區(qū)域
  • 如果我們沒(méi)有找到op_target,我們將會(huì)檢查這個(gè)異常是不是KeyError
  • goto error;是一種在 main loop 中拋出異常的方法
  • PyErr_Clear()抑制了當(dāng)前異常的拋出阱州,而DISPATCH()觸發(fā)了下一個(gè)操作碼的執(zhí)行

下一步就是收集我們想要的堆棧信息挑秉。

從底層帶你理解Python中的一些內(nèi)部機(jī)制

最后一步就是調(diào)用我們的回調(diào)函數(shù)!我們用call_function來(lái)搞定這件事苔货,我們通過(guò)研究操作碼CALL_FUNCTION的實(shí)現(xiàn)來(lái)學(xué)習(xí)怎么使用call_function犀概。

從底層帶你理解Python中的一些內(nèi)部機(jī)制

有了上面這些信息,我們終于可以搗鼓出一個(gè)操作碼DEBUG_OP的草稿了:

從底層帶你理解Python中的一些內(nèi)部機(jī)制

在編寫(xiě) CPython 實(shí)現(xiàn)的 C 代碼方面我確實(shí)沒(méi)有什么經(jīng)驗(yàn)夜惭,有可能我漏掉了些細(xì)節(jié)姻灶。如果您有什么建議還請(qǐng)您糾正,我期待您的反饋诈茧。

編譯它木蹬,成了!

一切看起來(lái)很順利若皱,但是當(dāng)我們嘗試去使用我們定義的操作碼DEBUG_OP的時(shí)候卻失敗了镊叁。自從 2008 年之后,Python 使用預(yù)先寫(xiě)好的 goto(你也可以從 這里獲取更多的訊息)走触。故晦譬,我們需要更新下 goto jump table,我們?cè)?Python/opcode_targets.h 中做如下修改互广。

從底層帶你理解Python中的一些內(nèi)部機(jī)制

這就完事了敛腌,我們現(xiàn)在就有了一個(gè)可以工作的新操作碼。唯一的問(wèn)題就是這貨雖然存在惫皱,但是沒(méi)有被人調(diào)用過(guò)像樊。接下來(lái),我們將DEBUG_OP注入到函數(shù)的字節(jié)碼中旅敷。

在 Python 字節(jié)碼中注入操作碼 DEBUG_OP

有很多方式可以在 Python 字節(jié)碼中注入新的操作碼:

  • 使用 peephole optimizer生棍, Quarkslab就是這么干的
  • 在生成字節(jié)碼的代碼中動(dòng)些手腳
  • 在運(yùn)行時(shí)直接修改函數(shù)的字節(jié)碼(這就是我們將要干的事兒)

為了創(chuàng)造出一個(gè)新操作碼,有了上面的那一堆 C 代碼就夠了∠彼現(xiàn)在讓我們回到原點(diǎn)涂滴,開(kāi)始理解奇怪甚至神奇的 Python!

我們將要做的事兒有:

  • 得到我們想要追蹤函數(shù)的 code object
  • 重寫(xiě)字節(jié)碼來(lái)注入DEBUG_OP
  • 將新生成的 code object 替換回去

和 code object 有關(guān)的小貼士

如果你從沒(méi)聽(tīng)說(shuō)過(guò) code object晴音,這里有一個(gè)簡(jiǎn)單的 介紹網(wǎng)路上也有一些相關(guān)的文檔可供查閱,可以直接Ctrl+F查找 code object

還有一件事情需要注意的是在這篇文章所指的環(huán)境中 code object 是不可變的:

從底層帶你理解Python中的一些內(nèi)部機(jī)制

但是不用擔(dān)心柔纵,我們將會(huì)找到方法繞過(guò)這個(gè)問(wèn)題的。

使用的工具

為了修改字節(jié)碼我們需要一些工具:

  • dis模塊用來(lái)反編譯和分析字節(jié)碼
  • dis.BytecodePython 3.4 新增的一個(gè)特性锤躁,對(duì)于反編譯和分析字節(jié)碼特別有用
  • 一個(gè)能夠簡(jiǎn)單修改 code object 的方法

用dis.Bytecode反編譯 code bject 能告訴我們一些有關(guān)操作碼搁料、參數(shù)和上下文的信息。

從底層帶你理解Python中的一些內(nèi)部機(jī)制

為了能夠修改 code object,我定義了一個(gè)很小的類用來(lái)復(fù)制 code object郭计,同時(shí)能夠按我們的需求修改相應(yīng)的值霸琴,然后重新生成一個(gè)新的 code object。

從底層帶你理解Python中的一些內(nèi)部機(jī)制

這個(gè)類用起來(lái)很方便拣宏,解決了上面提到的 code object 不可變的問(wèn)題沈贝。

從底層帶你理解Python中的一些內(nèi)部機(jī)制

測(cè)試我們的新操作碼

我們現(xiàn)在擁有了注入DEBUG_OP的所有工具杠人,讓我們來(lái)驗(yàn)證下我們的實(shí)現(xiàn)是否可用勋乾。我們將我們的操作碼注入到一個(gè)最簡(jiǎn)單的函數(shù)中:

從底層帶你理解Python中的一些內(nèi)部機(jī)制

看起來(lái)它成功了!有一行代碼需要說(shuō)明一下new_nop_code.co_stacksize += 3

  • co_stacksize 表示 code object 所需要的堆棧的大小
  • 操作碼DEBUG_OP往堆棧中增加了三項(xiàng)嗡善,所以我們需要為這些增加的項(xiàng)預(yù)留些空間

現(xiàn)在我們可以將我們的操作碼注入到每一個(gè) Python 函數(shù)中了辑莫!

重寫(xiě)字節(jié)碼

正如我們?cè)谏厦娴睦又兴吹降哪菢樱貙?xiě) Pyhton 的字節(jié)碼似乎 so easy罩引。為了在每一個(gè)操作碼之間注入我們的操作碼各吨,我們需要獲取每一個(gè)操作碼的偏移量,然后將我們的操作碼注入到這些位置上(把我們操作碼注入到參數(shù)上是有壞處大大滴)袁铐。這些偏移量也很容易獲取揭蜒,使用dis.Bytecode ,就像這樣 剔桨。

從底層帶你理解Python中的一些內(nèi)部機(jī)制

基于上面的例子屉更,有人可能會(huì)想我們的insert_op_debug會(huì)在指定的偏移量增加一個(gè)"\x00",這尼瑪是個(gè)坑叭髯骸瑰谜!我們第一個(gè)DEBUG_OP注入的例子中被注入的函數(shù)是沒(méi)有任何的分支的,為了能夠?qū)崿F(xiàn)完美一個(gè)函數(shù)注入函數(shù)insert_op_debug我們需要考慮到存在分支操作碼的情況树绩。

Python 的分支一共有兩種:

  • 絕對(duì)分支:看起來(lái)是類似這樣子的Instruction_Pointer = argument(instruction)
  • 相對(duì)分支:看起來(lái)是類似這樣子的Instruction_Pointer += argument(instruction)

相對(duì)分支總是向前的

我們希望這些分支在我們插入操作碼之后仍然能夠正常工作萨脑,為此我們需要修改一些指令參數(shù)。以下是其邏輯流程:

  • 對(duì)于每一個(gè)在插入偏移量之前的相對(duì)分支而言
  • 如果目標(biāo)地址是嚴(yán)格大于我們的插入偏移量的話饺饭,將指令參數(shù)增加 1
  • 如果相等渤早,則不需要增加 1 就能夠在跳轉(zhuǎn)操作和目標(biāo)地址之間執(zhí)行我們的操作碼DEBUG_OP
  • 如果小于,插入我們的操作碼的話并不會(huì)影響到跳轉(zhuǎn)操作和目標(biāo)地址之間的距離
  • 對(duì)于 code object 中的每一個(gè)絕對(duì)分支而言
  • 如果目標(biāo)地址是嚴(yán)格大于我們的插入偏移量的話瘫俊,將指令參數(shù)增加 1
  • 如果相等蛛芥,那么不需要任何修改,理由和相對(duì)分支部分是一樣的
  • 如果小于军援,插入我們的操作碼的話并不會(huì)影響到跳轉(zhuǎn)操作和目標(biāo)地址之間的距離

下面是實(shí)現(xiàn):

從底層帶你理解Python中的一些內(nèi)部機(jī)制

讓我們看一下效果如何:

從底層帶你理解Python中的一些內(nèi)部機(jī)制

甚好仅淑!現(xiàn)在我們知道了如何獲取堆棧信息和 Python 中每一個(gè)操作對(duì)應(yīng)的幀信息。上面結(jié)果所展示的結(jié)果目前而言并不是很實(shí)用胸哥。在最后一部分中讓我們對(duì)注入做進(jìn)一步的封裝涯竟。

增加 Python 封裝

正如您所見(jiàn)到的,所有的底層接口都是好用的。我們最后要做的一件事是讓 op_target 更加方便使用(這部分相對(duì)而言比較空泛一些庐船,畢竟在我看來(lái)這不是整個(gè)項(xiàng)目中最有趣的部分)银酬。

首先我們來(lái)看一下幀的參數(shù)所能提供的信息,如下所示:

  • f_code當(dāng)前幀將執(zhí)行的 code object
  • f_lasti當(dāng)前的操作(code object 中的字節(jié)碼字符串的索引)

經(jīng)過(guò)我們的處理我們可以得知DEBUG_OP之后要被執(zhí)行的操作碼筐钟,這對(duì)我們聚合數(shù)據(jù)并展示是相當(dāng)有用的揩瞪。

新建一個(gè)用于追蹤函數(shù)內(nèi)部機(jī)制的類:

  • 改變函數(shù)自身的co_code
  • 設(shè)置回調(diào)函數(shù)作為op_debug的目標(biāo)函數(shù)

一旦我們知道下一個(gè)操作,我們就可以分析它并修改它的參數(shù)篓冲。舉例來(lái)說(shuō)我們可以增加一個(gè)auto-follow-called-functions的特性李破。

從底層帶你理解Python中的一些內(nèi)部機(jī)制

現(xiàn)在我們實(shí)現(xiàn)一個(gè) Trace 的子類,在這個(gè)子類中增加 callback 和 doreport 這兩個(gè)方法壹将。callback 方法將在每一個(gè)操作之后被調(diào)用嗤攻。doreport 方法將我們收集到的信息打印出來(lái)。

這是一個(gè)偽函數(shù)追蹤器實(shí)現(xiàn):

從底層帶你理解Python中的一些內(nèi)部機(jī)制
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末诽俯,一起剝皮案震驚了整個(gè)濱河市妇菱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌暴区,老刑警劉巖闯团,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異仙粱,居然都是意外死亡房交,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)缰盏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)涌萤,“玉大人,你說(shuō)我怎么就攤上這事口猜「合” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵济炎,是天一觀的道長(zhǎng)川抡。 經(jīng)常有香客問(wèn)我,道長(zhǎng)须尚,這世上最難降的妖魔是什么崖堤? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮耐床,結(jié)果婚禮上密幔,老公的妹妹穿的比我還像新娘。我一直安慰自己撩轰,他們只是感情好胯甩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布昧廷。 她就那樣靜靜地躺著,像睡著了一般偎箫。 火紅的嫁衣襯著肌膚如雪木柬。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,573評(píng)論 1 305
  • 那天淹办,我揣著相機(jī)與錄音眉枕,去河邊找鬼。 笑死怜森,一個(gè)胖子當(dāng)著我的面吹牛速挑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播塔插,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼梗摇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼拓哟!你這毒婦竟也來(lái)了想许?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤断序,失蹤者是張志新(化名)和其女友劉穎流纹,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體违诗,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡漱凝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了诸迟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茸炒。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖阵苇,靈堂內(nèi)的尸體忽然破棺而出壁公,到底是詐尸還是另有隱情,我是刑警寧澤绅项,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布紊册,位于F島的核電站,受9級(jí)特大地震影響快耿,放射性物質(zhì)發(fā)生泄漏囊陡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一掀亥、第九天 我趴在偏房一處隱蔽的房頂上張望撞反。 院中可真熱鬧,春花似錦搪花、人聲如沸遏片。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)丁稀。三九已至吼拥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間线衫,已是汗流浹背凿可。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留授账,地道東北人枯跑。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像白热,于是被迫代替她去往敵國(guó)和親敛助。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 下面博文將帶你創(chuàng)建一個(gè)字節(jié)碼級(jí)別的追蹤API以追蹤Python的一些內(nèi)部機(jī)制屋确,比如類似YIELDVALUE纳击、YIE...
    嗨學(xué)編程閱讀 219評(píng)論 0 0
  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,798評(píng)論 0 38
  • 個(gè)人筆記,方便自己查閱使用 Py.LangSpec.Contents Refs Built-in Closure ...
    freenik閱讀 67,705評(píng)論 0 5
  • 包(lib)攻臀、模塊(module) 在Python中焕数,存在包和模塊兩個(gè)常見(jiàn)概念。 模塊:編寫(xiě)Python代碼的py...
    清清子衿木子水心閱讀 3,807評(píng)論 0 27
  • 風(fēng)入地平側(cè)使面刨啸, 轉(zhuǎn)視飛蘆滿襟間堡赔。 雨落凡間多轉(zhuǎn)衍, 涌沒(méi)幾泉意歸遠(yuǎn)设联。
    從半閱讀 149評(píng)論 0 1