使用python栈帧进行逃逸的探究
2024-06-08 14:12:29

一次使用python栈帧进行逃逸的demo探究

为什么会有这篇文章?

起初,我在讨论群里发起讨论,起初我只是不太理解这个demo为什么不能逃逸(即为什么f_back后为None

可惜群友们也不太清楚诶/(ㄒoㄒ)/~~

image-20240601193716401

于是打算研究下。

后面把demo完善,也就抽象出来了这个问题:使用栈帧逃逸的本质是什么?

我把最终的demo设计成这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
key = "flag{test}"
codes = '''import pdb
# 生成器函数
def f():
while True:
print("g.gi_frame in f:", g.gi_frame)
print("g.gi_frame.f_back in f:", g.gi_frame.f_back)
print("g.gi_frame.f_back.f_back in f:", g.gi_frame.f_back.f_back)
# pdb.set_trace()
yield g.gi_frame.f_back

g = f()

frame = next(g)
next(g)
next(g)

frame2 = g.gi_frame
# pdb.set_trace()
print(frame2)
print(frame2.f_back) # None
'''
locals = {}
code = compile(codes,"test2","exec")
exec(code, locals)

​ 尝试执行后的结果下图。

​ 可以发现三次next后结果大差不差,区别就在g.gi_frame.f_back中的行号:

image-20240601194247087

​ 仔细比对,可以发现刚好对应的就是三次nextcodes中的行号,这就是在不同位置调用生成器函数f时,在生成器函数fgi_frame.f_back的对应栈帧在不同位置,同时我们注意到一个细节print("g.gi_frame in f:", g.gi_frame) print(frame2)的打印结果不同,但是它们确实是同一个对象(print(is)进行确认)

这里也就要开始涉及python虚拟机底层的知识了

  • 首先是栈帧的数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* previous frame, or NULL */
PyCodeObject *f_code; /* code segment */
PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
PyObject *f_globals; /* global symbol table (PyDictObject) */
PyObject *f_locals; /* local symbol table (any mapping) */
PyObject **f_valuestack; /* points after the last local */
/* Next free slot in f_valuestack. Frame creation sets to f_valuestack.
Frame evaluation usually NULLs it, but a frame that yields sets it
to the current stack top. */
PyObject **f_stacktop;
PyObject *f_trace; /* Trace function */

/* In a generator, we need to be able to swap between the exception
state inside the generator and the exception state of the calling
frame (which shouldn't be impacted when the generator "yields"
from an except handler).
These three fields exist exactly for that, and are unused for
non-generator frames. See the save_exc_state and swap_exc_state
functions in ceval.c for details of their use. */
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
/* Borrowed reference to a generator, or NULL */
PyObject *f_gen;

int f_lasti; /* Last instruction if called */
/* Call PyFrame_GetLineNumber() instead of reading this field
directly. As of 2.3 f_lineno is only valid when tracing is
active (i.e. when f_trace is set). At other times we use
PyCode_Addr2Line to calculate the line from the current
bytecode index. */
int f_lineno; /* Current line number */
int f_iblock; /* index in f_blockstack */
char f_executing; /* whether the frame is still executing */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
} PyFrameObject;
  • 栈帧的创建和销毁

​ 栈帧的创建和销毁是动态的,随着函数的调用和返回而不断发生。当一个函数被调用时,一个新的栈帧会被创建并推入调用栈,当函数调用结束后,对应的栈帧会从调用栈中弹出并销毁

  • 函数调用栈

f_back串起了栈帧列表:

img

其实到这里也渐渐明晰了

在 Python 中,一个栈帧的 f_back 属性指向调用它的栈帧。对于在模块(全局)级别执行的代码,其所在的栈帧的 f_back 通常是 None,因为这些代码并没有被另一个函数或方法调用,实际上,它们是在模块的顶级或全局作用域内执行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
key = "flag{test}"
codes = '''import pdb
# 生成器函数
def f():
while True:
print("g.gi_frame in f:", g.gi_frame)
print("g.gi_frame.f_back in f:", g.gi_frame.f_back)
print("g.gi_frame.f_back.f_back in f:", g.gi_frame.f_back.f_back)
# pdb.set_trace()
yield g.gi_frame.f_back

g = f()

frame = next(g)
next(g)
next(g)

frame2 = g.gi_frame
# pdb.set_trace()
print(frame2)
print(frame2.f_back) # None
'''
locals = {}
code = compile(codes,"test2","exec")
exec(code, locals)

(先后解开代码中的两处pdb注释可得如下结果)

  • 函数外

image-20240601202724116

直接在全局下使用g.gi_frame获取到的栈帧对象,是直接指向<model>全局的,回退之后为None(对于在模块(全局)级别执行的代码,其所在的栈帧的 f_back 通常是 None

image-20240601202909134

  • 函数内的栈帧分析

    image-20240601202643113

​ 能从f()栈帧回退拿到codes所在的”全局”栈帧,再回退就能拿到exec所在的栈帧了,此为逃逸

image-20240601203606382

  • 回过头来,再回答开头这个问题:使用栈帧逃逸的本质是什么?或者说我们需要拿到什么?

可以发现,我们应该要拿到调用栈上的一个函数的栈帧

参考

https://www.cnblogs.com/Chang-LeHung/p/17351403.html

Prev
2024-06-08 14:12:29
Next