在字节码的生成中,我搞明白了dis如何展示字节码。
当“字节码”放在我面前的时候,我能猜到某些位置上的含义,但还有些位置猜不到。我想弄清楚,这些猜不到的部分,具体的含义。
3 0 LOAD_CONST 0 (<code object get_func at 00000000034EA7B0, file "closure.py", line 3>)
3 MAKE_FUNCTION 0
6 STORE_NAME 0 (get_func)
12 9 LOAD_NAME 0 (get_func)
12 CALL_FUNCTION 0
15 STORE_NAME 1 (show_value)
13 18 LOAD_NAME 1 (show_value)
21 CALL_FUNCTION 0
24 POP_TOP
25 LOAD_CONST 1 (None)
28 RETURN_VALUE
将上次Python2生成的字节码摘抄在这里,由此能够看出来,一条字节码由5个部分组成:
- 一个数字,可以猜到,是行号;
- 又一个数字,猜不到;
- 一行字符串,猜得到,指令的名字;
- 还有一个数字,看起来像是参数的个数;
- 又一个括号包起来的字符串,具体的参数?
不过,从Python交互环境下面粘贴过来,在markdown中展现的内容,不止如此:
第一个数字和第二个数字中间,有3个小虚线,是否意味着这里面,还藏着2个不知名数据呢?
我找了几个很大的文件,反汇编了一下,发现中间只会多出一个符号“>>”,这个符号又是什么意思呢?
574 >> 226 LOAD_CONST 4 ('')
228 STORE_FAST 6 (opt)
以上,是我初次看到字节码所长的样子,并猜测着各个位置上的意思。
继续网上冲浪,寻找这字节码的各个位置含义。最终让我找到一篇超赞的文章,它让我理解了dis.dis(*args)所生成的结果。
事实上,dis生成结果只是Python为程序员所设计的易于理解的,反汇编(Disassembling )之后的版本。每一条字节码本质上是由两个字节存储的一条指令,有以下形式:
# 一个指令编号,后面跟着一个指令参数
opcode oparg
opcode oparg
opcode(指令编号)
一个小于等于255的正整数,代表了具体指令是什么。它对应一个易于理解的opname,这个opname可以在两个地方看到:
- Python源码中的Include/opcode.h文件;
- dis模块的opname dict。
import dis
print(dis.opname[101]) # 'LOAD_NAME'
oparg(指令参数)
对应指令的参数,每一个参数,都根据opcode的不同,而拥有不同的意义。
有两个需要注意的点是:
- 有一些指令不需要参数,Python用编号来进行区分,小于dis.HAVE_ARGUMENT(Python3.9.1中是90)的指令没有参数,大于等于dis.HAVE_ARGUMENT的指令有参数。
- 另外一些指令,参数可能会大于255,1个字节存储不了,则会在该指令前再插入一条EXTENDED_ARG指令,辅助存储。每一条指令前,最多只能插入3条EXTENDED_ARG。
// Python3.9.1...\Objects\frameobject.c
// 真正arg的计算方式
/* Given the index of the effective opcode,
scan back to construct the oparg with EXTENDED_ARG */
static unsigned int
get_arg(const _Py_CODEUNIT *codestr, Py_ssize_t i)
{
_Py_CODEUNIT word;
unsigned int oparg = _Py_OPARG(codestr[i]);
if (i >= 1 && _Py_OPCODE(word = codestr[i-1]) == EXTENDED_ARG) {
oparg |= _Py_OPARG(word) << 8;
if (i >= 2 && _Py_OPCODE(word = codestr[i-2]) == EXTENDED_ARG) {
oparg |= _Py_OPARG(word) << 16;
if (i >= 3 && _Py_OPCODE(word = codestr[i-3]) == EXTENDED_ARG) {
oparg |= _Py_OPARG(word) << 24;
}
}
}
return oparg;
}
按照我的理解,oparg,只是一个偏移量,它根据opcode所属于的组,去对应的数据结构(co_consts、co_names、co_varnames、co_cellvars + co_freevars、co_lnotab等)中拿真正的数据。
根据以上的了解,我搞清楚了各个位置的意义:
- 一个数字,是代码的行号;
- 又一个数字,指令在字节码序列(co_code)中的偏移量;
- 一行字符串,指令的名字;
- 还有一个数字,oparg,指令的参数;
- 又一个括号包起来的字符串,根据oparg取得的参数实际值;
- 那个不常见的符号“>>”,标识循环(异常、if else判断等)的起止点。
参考资料
- 字节码的一篇介绍,opensource.com/article/18/…
- 那一篇超赞的文章,建议阅读,towardsdatascience.com/understandi…