Python 已經成為最流行的編程語言之一,大量 Python 包采用 Python/C 的多語言架構,其中宿主語言 Python 和外部語言 C/C++ 的結合兼具開發效率與性能,被包括 NumPy、Pillow、TensorFlow 和 PyTorch 等在內的諸多主流軟件系統所采用。但是 Python 和 C/C++ 之間語言特性的差異也使得基于 Python/C API 的跨語言接口代碼容易出錯,類型誤用就是常見的錯誤之一。
# Python 程序的靜態類型推斷
學界和業界都在 Python 的靜態類型推斷這一問題上做了大量嘗試。這些工作可以分為以下三類:
通過擴展語法支持類型標注,并基于類型標注進行類型推斷。該方法的弊端在于需要修改源碼且影響了 Python 開發的敏捷性。
基于機器學習的方法。一方面該方法的數據集往往來自傳統方法的推斷結果,另一方面該方法是非確定性的,只能得到概率的結果。
基于數據流分析、抽象解釋、SMT 求解等傳統程序分析方法。這些工作對外部對象的處理往往采用直接忽略(如視為 object 類型)或預置類型存根(type stubs)等方法,類型推斷精度表現不佳或需要人工輔助。
# 研究方法與定義#
# 外部接口
Python 的外部接口 Python/C API 是橋接 Python 和 C/C++ 的中間層。如圖 1 所示,外部函數 ext.foo 通過 Python/C API PyMethodDef 映射到 C 實現 _foo,再通過 Python/C API PyModuleDef 關聯到模塊 ext。在 C 實現內部,Python/C API PyArg_ParseTuple 是一類常見的參數解析方法,它通過格式化串指明由 Python 到 C 的類型轉換。Python/C API PyLong_FromLong 是一類常見的返回類型轉換,它把一個 C 整型變量轉換到 Python 整型變量。
圖 1:Python 的 C 擴展模塊的一個例子
對于靜態類型語言,外部函數在聲明時帶有顯式類型,動態類型語言雖然沒有這一信息(如圖 1(b)第 2 行),但在接口層仍需給出包含跨語言的類型轉換等信息的調用接口描述。我們的核心思路就是建模并分析這些調用接口描述中的隱式信息,推斷作用于外部函數的類型約束。
圖 2:多語言與類型系統視角下的外部函數調用
圖 2 是類型系統視角下的外部函數調用。如果僅僅從單語言視角來看(藍色虛線),外部函數的參數類型和返回類型都是不可知的(灰色框);但在多語言的視角下(紅色實線),結合跨語言接口層得到的調用接口描述,外部函數的類型實際是可推導的。我們把這些隱式信息分成三個部分:
外部函數聲明(D)建立 Python 側外部函數的調用名和 C 側外部函數實現之間的映射關系。
參數類型轉換(P)刻畫 Python 側傳入外部函數的實參到 C 變量的類型轉換。
返回類型轉換(R)刻畫 C 側返回值返回 Python 側時的類型轉換。
# 抽象語法
我們形式化地把 Python/C 多語言軟件系統的抽象語法表示為圖 3,其中上標 p 和 c 標記不同的語言側。
圖 3:Python/C 多語言系統的抽象語法
不同于本地函數在 Python 側聲明與定義,并在 Python 側應用,外部函數的應用在 Python 側,但其聲明和定義都在 C 側。
# 類型
作為動態類型語言,如圖 4 所示,Python 側的類型是 Python 變量在運行時被綁定的類型值,包括 str、int、object 等內置類型;同時我們引入函數類型 pFunc 表示函數,引入積類型 pProduct 表示 list、tuple、dict 等類型,引入和類型 pUnion 支持共用體這一 C 側常見的語言特性。一些類型如 module、iterator 等不在類型集合中,因為它們在傳遞給外部函數時會被作為 object 類型對象處理。
圖 4:Python 側類型
Python/C 跨語言接口層的調用接口描述能夠給出更嚴格的類型和值約束。比如圖 1 中,Python/C API 函數 PyArg_ParseTuple 的第 2 個參數給出的格式化串 II 中的格式化單元I要求傳入對應的外部函數的實參是一個 Python 整型并且非負。我們利用子類型來刻畫這類規則。Python 側類型的子定型規則如圖 5 所示。
圖 5:Python 側類型的子定型規則
如圖 6 所示,C 側除了常見的內置類型外,還包括一些在 CPython 解釋器內部的、與 Python 側類型實現相對應的結構體。作為 Python/C API 的一部分,它們也被用于在接口層接收 Python 側傳遞并轉換的對象。
圖 6:C 側類型
# 類型推斷#
在以上核心思路和文法定義的基礎上,我們把類型推斷規則形式化地表示為如下形式:
外部函數聲明(D)、參數類型轉換(P)、返回類型轉換(R)共同構成推理前提,從而推導出包含在 Python/C 跨語言接口層調用接口描述中的外部函數的類型簽名,函數類型的參數類型和返回類型由 D、P、R 的具體組合確定。
# 外部函數聲明 D
表示如上,它描述了 Python 側外部函數調用名和 C 側實現的映射關系。其中flag給出調用慣例,在 CPython 中典型的如METH_VARARGS,它表示外部函數接收一個或多個 Python 對象作為參數,它們被打包成一個對象并傳遞到 C 側,跨語言接口層需要給出把這一打包對象解析到多個 C 變量的規則。
# 參數類型轉換 P
表示如上,它刻畫了基于程序性質 P 在 C 側(包含跨語言接口代碼)的上下文中描述的 Python 類型到 C 類型的轉換。這種隱式信息的分析包含了以下兩類常見的規則:
## 調用慣例分析
例如,當調用慣例flag為METH_NOARGS時,外部函數被聲明為無參的,表示為如下的無參分析(Parameter-Free Analysis),
假設判斷 表示只有當關于程序性質 P 的門限語義謂詞 為真時,判斷 J 成立。
然而,當對應的 C 實現根本不解析并使用傳入的參數時,外部函數實際也是無參的?;谝粋€未使用形參的分析(Unused Parameter Analysis)可以類似地表示如下,
動態類型語言(Python)和靜態類型語言(C/C++)之間類型系統的差異,以及 Python 外部接口的設計導致了這種聲明上的冗余,并且留下了聲明不一致的隱患。即只有當上述兩個門限語義謂詞都為真時,才可以確定外部函數是無參的,用合取范式表示如下:
## 參數解析分析
當上述兩個門限語義謂詞都為假時,外部函數至少接收一個 Python 對象。如上所述,這些 Python 側對象被打包為跨語言接口層的一個中間參數,該參數被解析并恢復到若干個 C 變量。最常見的,這一跨語言的類型轉換是由參數解析族 Python/C API 完成。這些 Python/C API 用一個格式化串指明轉換規則,一個格式化串包含零或多個格式化單元,每個格式化單元(特殊含義字符除外)對應一個 Python 類型到 C 類型的轉換,表示如下:
其中 是某個參數解析族的 Python/C API(常見的如 PyArg_ParseTuple), 是其第 i 個格式化單元。例如,格式化單元 對應 Python 非負整型到 C 無符號 int 類型的轉換, ,完整的格式化單元轉換規則表見論文表 1。
# 返回類型轉換 R
表示如上,它刻畫了基于程序性質 P 在 C 側(包含跨語言接口)的上下文中描述的 C 類型到 Python 類型的轉換。作為 Python 的一部分,其外部函數也支持多返回(multiple returns)的語言特性,而 C 本身是不支持的。這部分隱式信息的分析包含四類常見的規則。
##值構建分析
同樣基于格式化串,但是方向與參數類型轉換的參數解析分析相反,即:
是某個值構建族的 Python/C API(常見的如 Py_BuildValue), 是其第 j 個格式化單元,m 個 C 變量根據對應的格式化單元轉換到 Python 對象并共同構成一個 tuple 對象作為多返回的值。
## 顯性轉換分析
一些 Python/C API 支持直接以單一對象作為外部函數的返回。
顯式轉換的 Python/C API 形如:(1)PyPT_FromCT 把一個 CT 類型的 C 變量轉換到一個 PT 類型的 Python 變量,(2)PyPT_New 創建并返回一個 PT 類型的 Python 變量,(3)Py_PT 本身直接作為一個 PT 類型的對象被返回到 Python 側。
## 類型轉換(type cast)分析
C 程序支持作為右結合算子的顯式類型轉換,對于一個形如 的返回表達式,可以推斷:
作為外部函數的 C 實現向 Python 側的返回,C 類型 是與 Python 內置類型一一對應的結構體(圖 6 中 Py 開頭的 C 類型)。
## 可達定義分析
考慮如下圖所示的更復雜的返回情形,
圖 7:一個復雜的返回類型轉換的例子
變量 result 被聲明為 T1 類型(一般為 PyObject*),其可能通過調用前述的一些 Python/C API 被賦值為更精化的類型(T2,T3)。我們通過一個過程內的可達定義分析 來分析這樣的類型傳播。 內部會調用前面三類的返回類型轉換分析?;诳蛇_定義分析的返回類型轉換表示如下:
# 小結
對于類型推斷(TInfer)的三個前提,外部函數聲明(D)只有一種形式,參數類型轉換(P)包含形式(Pcc)和(Pap),返回類型轉換(R)包含形式(Rvb),(Rec),(Rtc)和(Rrd)。這樣,對于使用參數解析分析進行參數類型轉換、使用值構建分析進行返回類型轉換的一個外部函數典型模式,其類型推斷規則如下:
類似地,帶有顯式返回的無參外部函數可以推斷如下:
# 實驗結果#
我們的靜態類型推斷系統 PyCType 的原型結構如圖 8 所示。
圖 8:PyCType 架構概覽
接口分離器從 Python/C 多語言項目中分離出跨語言接口代碼。預處理配置器配置解析文件所需的依賴。AST 解析器基于 Python 實現的 C99 解析器 pycparser。在得到接口代碼的 AST 后,多數分析基于訪問 AST 實現,當某個分析需要其他中間表示如 CFG 時,AST 變換模塊對對應的 AST 片段進行變換。其他主要處理模塊(圓角矩形)與前文對應。
# 外部函數聲明與其實現不一致的漏洞
如調用慣例分析小節所述,同一個外部函數其無參分析(PFA)和未使用參數分析(UPA)可能不能同時成立,這會導致一個無參外部函數可以接受任意類型的參數。
# 可靠性
類型推斷的可靠性是構建以上嚴格的推理系統的主要目的之一,即類型推斷的結果沒有錯誤(但可能不夠精確,如把 int 推斷為 object)。我們通過人工檢查驗證了該靜態類型推斷系統的可靠性。同時漏洞發現也是沒有誤報的,所有錯誤都可以構造出對應的觸發代碼。(這里的可靠性是對類型推斷而言的。在漏洞檢查的研究中,可靠一般指沒有漏報。如果將一致性漏洞檢查作為一個獨立的系統,其應表示為類型推斷系統調用慣例分析的謂詞條件的否定命題。)
# 完備性
在可靠的基礎上,完備性成為衡量類型推斷系統有效性的一個重要指標,即推斷率。如表 1 所示是 CPython、NumPy 和 Pillow 中外部函數的參數類型的推斷率??梢钥吹?,對于參數類型轉換,規則(Pcc)和(Pap)能夠覆蓋大多數的情形。同時在規則中描述更多符合條件的 Python/C API 并不困難。
表 1:參數類型推斷的完備性
一方面,參數個數往往多于返回值,一方面,外部函數調用作為 Python 程序的一部分,其返回值可能是其他(外部或本地)函數的參數。因此,整個系統的有效性需要和已有的單語言的類型推斷工具結合起來進行評估。
# 有效性
由于可靠并不等于精確,因此我們選擇上表中推斷率最高的 Pillow 來構建類型增強實驗以進一步衡量有效性。來自 Google 的 Pytype 是 state-of-the-art 的 Python 靜態類型推斷工具,其不支持外部函數的自動推斷,而是通過類型存根預置了一些外部函數的類型簽名,如常見的標準庫函數。我們把對 Pillow 中外部函數類型推斷的結果編碼成 Pytype 的類型存根,然后比較這一類型增強前后的推斷率。實驗目標我們選擇 GitHub 中使用了 Pillow 且星標多于 3 萬的 Python 倉庫,實驗結果如表 2 所示,可以看到,PyCType 對 Pytype 有 7% 到 80% 的提升(平均 27.5%)。
表 2:類型推斷增強實驗
# 總結#
我們提出了 Python 外部函數的靜態類型推斷系統 PyCType。其類型推斷規則包括三部分可組合的推理前提,分別建模和分析 Python/C 跨語言接口層中類型轉換相關的隱式信息。在主流軟件系統上的實驗表明,PyCType 能夠可靠地推斷多數外部函數的類型簽名。其作為單語言 Python 靜態類型推斷工具的增強,使其能夠推斷含有外部函數調用的程序。同時能夠檢查使得無參外部函數可以接受任意類型參數的聲明不一致的漏洞,部分發現漏洞已被確認和修復。
審核編輯:劉清
-
smt
+關注
關注
40文章
2883瀏覽量
69061 -
機器學習
+關注
關注
66文章
8378瀏覽量
132425 -
python
+關注
關注
56文章
4782瀏覽量
84463
原文標題:技術干貨 | Python 的 C 外部函數的靜態類型推斷
文章出處:【微信號:編程語言Lab,微信公眾號:編程語言Lab】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論