12

Given a C Python frame pointer, how do I look at arbitrary evaluation stack entries? (Some specific stack entries can be found via locals(), I'm talking about other stack entries.)

I asked a broader question like this a while ago:

getting the C python exec argument string or accessing the evaluation stack

but here I want to focus on being able to read CPython stack entries at runtime.

I'll take a solution that works on CPython 2.7 or any Python later than Python 3.3. However if you have things that work outside of that, share that and, if there is no better solution I'll accept that.

I'd prefer not modifying the C Python code. In Ruby, I have in fact done this to get what I want. I can speak from experience that this is probably not the way we want to work. But again, if there's no better solution, I'll take that. (My understanding wrt to SO points is that I lose it in the bounty either way. So I'm happy go see it go to the person who has shown the most good spirit and willingness to look at this, assuming it works.)

update: See the comment by user2357112 tldr; Basically this is hard-to-impossible to do. (Still, if you think you have the gumption to try, by all means do so.)

So instead, let me narrow the scope to this simpler problem which I think is doable:

Given a python stack frame, like inspect.currentframe(), find the beginning of the evaluation stack. In the C version of the structure, this is f_valuestack. From that we then need a way in Python to read off the Python values/objects from there.

update 2 well the time period for a bounty is over and no one (including my own summary answer) has offered concrete code. I feel this is a good start though and I now understand the situation much more than I had. In the obligatory "describe why you think there should be a bounty" I had listed one of the proffered choices "to draw more attention to this problem" and to that extent where there had been something less than a dozen views of the prior incarnation of the problem, as I type this it has been viewed a little under 190 times. So this is a success. However...

If someone in the future decides to carry this further, contact me and I'll set up another bounty.

Thanks all.

rocky
  • 7,226
  • 3
  • 33
  • 74
  • Rocky, can you add some details to the question? Do you want python-only solution which will works with original CPython or you can modify CPython? What is exact version of CPython used? Is there backtrace printing in the CPython in case of exception or fatal error? Do you want only backtrace (which function was called by which) or you want to find and access local variables stored on stack? – osgx Jun 08 '17 at 14:02
  • @osgx question revised. I suspect if you get backtrace working, you'll be able to do it generally. But to simplify things, if you have a solution that works only on Python frame pointers other than the current one, I'll accept that, assuming there's no better solution. – rocky Jun 08 '17 at 14:31
  • 1
    Rocky, could you give some more info about what you are trying to achieve conceptually, and about the context? E.g. are you writing a debugger, or what information is it that you want from the stack frames? Where/how do you get hold of a stack frame, as part of an exception, a custom debugger, etc.? I (we) don't want to answer with a lot of potentially irrelevant code. – Abraham Jun 08 '17 at 15:14
  • @Abraham see the discussion and chat. If this still isn't enough let me know. – rocky Jun 09 '17 at 00:13
  • Can you add some information or background on what this is? You are not talking about the stacktrace or traceback, right? Because from a Python frame, you can trivially get the stack (`f_back`). – Albert Mar 10 '22 at 12:45
  • Ah, you refer to the stack-based VM of the Python interpreter, specifically the stack of the local vars during one point in time? Maybe some link for further reading resources on these CPython implementation details would be nice, or just mentioning stack-based VM. – Albert Mar 10 '22 at 12:52

4 Answers4

14

This is sometimes possible, with ctypes for direct C struct member access, but it gets messy fast.

First off, there's no public API for this, on the C side or the Python side, so that's out. We'll have to dig into the undocumented insides of the C implementation. I'll be focusing on the CPython 3.8 implementation; the details should be similar, though likely different, in other versions.

A PyFrameObject struct has an f_valuestack member that points to the bottom of its evaluation stack. It also has an f_stacktop member that points to the top of its evaluation stack... sometimes. During execution of a frame, Python actually keeps track of the top of the stack using a stack_pointer local variable in _PyEval_EvalFrameDefault:

stack_pointer = f->f_stacktop;
assert(stack_pointer != NULL);
f->f_stacktop = NULL;       /* remains NULL unless yield suspends frame */

There are two cases in which f_stacktop is restored. One is if the frame is suspended by a yield (or yield from, or any of the multiple constructs that suspend coroutines through the same mechanism). The other is right before calling a trace function for a 'line' or 'opcode' trace event. f_stacktop is cleared again when the frame unsuspends, or after the trace function finishes.

That means that if

  • you're looking at a suspended generator or coroutine frame, or
  • you're currently in a trace function for a 'line' or 'opcode' event for a frame

then you can access the f_valuestack and f_stacktop pointers with ctypes to find the lower and upper bounds of the frame's evaluation stack and access the PyObject * pointers stored in that range. You can even get a superset of the stack contents without ctypes with gc.get_referents(frame_object), although this will contain other referents that aren't on the frame's stack.

Debuggers use trace functions, so this gets you value stack entries for the top stack frame while debugging, most of the time. It does not get you value stack entries for any other stack frames on the call stack, and it doesn't get you value stack entries while tracing an 'exception' event or any other trace events.


When f_stacktop is NULL, determining the frame's stack contents is close to impossible. You can still see where the stack begins with f_valuestack, but you can't see where it ends. The stack top is stored in a C-level stack_pointer local variable that's really hard to access.

  • There's the frame's code object's co_stacksize, which gives an upper bound on the stack size, but it doesn't give the actual stack size.
  • You can't tell where the stack ends by examining the stack itself, because Python doesn't null out the pointers on the stack when it pops entries.
  • gc.get_referents doesn't return value stack entries when f_stacktop is null. It doesn't know how to retrieve stack entries safely in this case either (and it doesn't need to, because if f_stacktop is null and stack entries exist, the frame is guaranteed reachable).
  • You might be able to examine the frame's f_lasti to determine the last bytecode instruction it was on and try to figure out where that instruction would leave the stack, but that would take a lot of intimate knowledge of Python bytecode and the bytecode evaluation loop, and it's still ambiguous sometimes (because the frame might be halfway through an instruction). This would at least give you a lower bound on the current stack size, though, letting you safely inspect at least some of it.
  • Frame objects have independent value stacks that aren't contiguous with each other, so you can't look at the bottom of one frame's stack to find the top of another. (The value stack is actually allocated within the frame object itself.)
  • You might be able to hunt down the stack_pointer local variable with some GDB magic or something, but it'd be a mess.
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Ok. If it will simplify things, let's just deal with the frames other than the current frame. That way I think we can use `f_valuestack`, which if I understand you correctly is the bottom of the current current frame and if that's the case then the top of the previous frame. And I guess then everything from there to the prior value of `f_valuestack` before the last call will have what we want. Does this make sense? – rocky Jun 08 '17 at 19:47
  • 1
    @rocky: That won't work. Frames have completely independent value stacks; they're not contiguous like C stack frames. You can't look at the bottom of one stack frame to find the top of another. – user2357112 Jun 08 '17 at 19:49
  • Ok. But still the C program has to have a way to (re)set where the top of the stack is on returning from a function. How is that done? Some sort of C local variable? – rocky Jun 08 '17 at 22:44
  • @rocky: When returning from a Python function, the value stack of the stack frame for that function call is emptied out, and that value stack is never used again. If we're returning *to* another Python function, then at C level, execution reenters the corresponding `_PyEval_EvalFrameDefault` call, where the `stack_pointer` local variable records the top of that function call's value stack. – user2357112 Jun 08 '17 at 22:49
  • If it helps, I'm also happy to narrow things down to the `eval` and `exec` primitives. Can we say that those always set `f_stacktop`? – rocky Jun 08 '17 at 22:52
  • 3
    It's probably pretty confusing just how many stacks are involved here. There's the C stack, with (usually) one frame for each C function call, there are Python stack frames, most of them corresponding to Python function calls, and for each Python stack frame, there's a value stack, where Python bytecodes save values to operate on. The value stacks are independent stacks, not frames of a larger stack. The value stacks are what you want to inspect. – user2357112 Jun 08 '17 at 22:52
  • `eval` and `exec` go through the same `_PyEval_EvalFrameDefault` function as any other Python code execution, and they still null out `f_stacktop` and keep track of the top of the stack with that `stack_pointer` local variable. – user2357112 Jun 08 '17 at 22:55
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/146214/discussion-between-rocky-and-user2357112). – rocky Jun 08 '17 at 23:03
  • there's one more place where CPython leads stacktop to the outside world: at the beginning of a loop or a try block, it [pushes the stack level to the f_blockstack stack](https://github.com/python/cpython/blob/3.7/Python/ceval.c#L2852). this gets you to the stack top if you know whether you're inside a for loop (+1 the b_level field of f_blockstack) or inside a while loop (+0). – moos May 17 '20 at 01:03
  • 1
    @moos: It doesn't do that any more for loops in 3.8. SETUP_LOOP is gone. It was always a little silly that it existed, since the necessary stack level information for loop handling could always be determined statically. – user2357112 May 17 '20 at 02:40
  • Anyway, examining the block stack would only get you the stack top at the time each block was entered, not the current stack top. It lets you examine *some* of the value stack entries, but not necessarily all of them. – user2357112 May 17 '20 at 02:45
  • @user2357112supportsMonica thanks for pointing this out. you saved me a bunch of trouble down the line (here's what i'm working on: https://github.com/a-rahimi/python-checkpointing2) – moos May 17 '20 at 21:57
2

Note added later: See crusaderky's get_stack.py which might be worked into a solution here.

Here are two potential solution partial solutions, since this problem has no simple obvious answer, short of:

  • modifying the CPython interpreter or by:
  • instrumenting the bytecode beforand such as via x-python

Thanks to user2357112 for enlightenment on the difficulty of the problem, and for descriptions of:

  • the various Python stacks used at runtime,
  • the non-contiguous evaluation stack,
  • the transiency of the evaluation stack and
  • the stack pointer top which only lives as a C local variable (which at run time, might be or is likely only saved in the value of a register).

Now to potential solutions...

The first solution is to write a C extension to access f_valuestack which is the bottom (not top) of a frame. From that you can access values, and that too would have to go in the C extension. The main problem here, since this is the stack bottom, is to understand which entry is the top or one you are interested in. The code records the maximum stack depth in the function.

The C extension would wrap the PyFrameObject so it can get access to the unexposed field f_valuestack. Although the PyFrameObject can change from Python version to Python version (so the extension might have to check which python version is running), it still is doable.

From that use an Abstract Virtual Machine to figure out which entry position you'd be at for a given offset stored in last_i.

Something similar for my purposes would for my purposes would be to use a real but alternative VM, like Ned Batchhelder's byterun. It runs a Python bytecode interpreter in Python.

Note added later: I have made some largish revisions in order to support Python 2.5 .. 3.7 or so and this is now called x-python

The advantage here would be that since this acts as a second VM so stores don't change the running of the current and real CPython VM. However you'd still need to deal with the fact of interacting with external persistent state (e.g. calls across sockets or changes to files). And byterun would need to be extended to cover all opcodes and Python versions that potentially might be needed.

By the way, for multi-version access to bytecode in a uniform way (since not only does bytecode change a bit, but also the collections of routines to access it), see xdis.

So although this isn't a general solution it could probably work for the special case of trying to figure out the value of say an EXEC up which appear briefly on the evaluation stack.

rocky
  • 7,226
  • 3
  • 33
  • 74
  • That `get_stack.py` thing relies on `f_stacktop`, so it's not going to work for anything but the easy case of suspended generator and coroutine frames. (It's also got a few incorrect field type declarations.) – user2357112 May 18 '20 at 00:23
  • @user2357112supportsMonica Ok. That's why I wrote: "_might_ be worked into a solution here." – rocky May 18 '20 at 00:36
1

I wrote some code to do this. It seems to work so I'll add it to this question.

How it does it is by disassembling the instructions, and using dis.stack_effect to get the effect of each instruction on stack depth. If there's a jump, it sets the stack level at the jump target.

I think stack level is deterministic, i.e. it is always the same at any given bytecode instruction in a piece of code no matter how it was reached. So you can get stack depth at a particular bytecode by looking at the bytecode disassembly directly.

There's a slight catch which is that if you are in an active call, the code position is shown as last instruction being the call, but the stack state is actually that before the call. This is good because it means you can recreate the call arguments from the stack, but you need to be aware that if the instruction is a call that is ongoing, the stack will be at the level of the previous instruction.

Here's the code from my resumable exception thing that does this:

cdef get_stack_pos_after(object code,int target,logger):
    stack_levels={}
    jump_levels={}
    cur_stack=0
    for i in dis.get_instructions(code):
        offset=i.offset
        argval=i.argval
        arg=i.arg
        opcode=i.opcode
        if offset in jump_levels:
            cur_stack=jump_levels[offset]
        no_jump=dis.stack_effect(opcode,arg,jump=False)        
        if opcode in dis.hasjabs or opcode in dis.hasjrel:
            # a jump - mark the stack level at jump target
            yes_jump=dis.stack_effect(opcode,arg,jump=True)        
            if not argval in jump_levels:
                jump_levels[argval]=cur_stack+yes_jump
        cur_stack+=no_jump
        stack_levels[offset]=cur_stack
        logger(offset,i.opname,argval,cur_stack)
    return stack_levels[target]

https://github.com/joemarshall/unthrow

JoeM
  • 11
  • 1
0

I've tried to do this in this package. As others point out, the main difficulty is in determining the top of the Python stack. I try to do this with some heuristics, which I've documented here.

The overall idea is that by the time my snapshotting function is called, the stack consists of the locals (as you point out), the iterators of nested for loops, and any exception triplets currently being handled. There's enough information in Python 3.6 & 3.7 to recover these states and therefore the stacktop.

I also relied on a tip from user2357112 to pave a way to making this work in Python 3.8.

moos
  • 2,444
  • 2
  • 15
  • 14