В C Python доступ к стеку оценки байт-кода

Учитывая указатель фрейма C Python, как я могу посмотреть на произвольные записи стека оценки? (Некоторые конкретные записи стека можно найти через locals()Я говорю о других записях стека.)

Я задал более широкий вопрос, как это некоторое время назад:

получить строку аргумента exec C python или получить доступ к стеку оценки

но здесь я хочу сосредоточиться на возможности чтения записей стека CPython во время выполнения.

Я возьму решение, которое работает на CPython 2.7 или на любом Python позже Python 3.3. Однако, если у вас есть что-то, что работает вне этого, поделитесь этим, и, если нет лучшего решения, я приму это.

Я бы предпочел не изменять код на языке Python. В Ruby я фактически сделал это, чтобы получить то, что я хочу. По своему опыту могу сказать, что мы, наверное, не так хотим работать. Но опять же, если нет лучшего решения, я возьму это. (Мое понимание в отношении ТА пунктов состоит в том, что я теряю его в награду в любом случае. Поэтому я рад, что он дойдет до человека, который проявил самый хороший дух и готовность взглянуть на это, предполагая, что это работает.)

обновление: см. комментарий user2357112 tldr; В основном это трудно сделать невозможно.(Тем не менее, если вы думаете, что у вас есть желание попробовать, во что бы то ни стало.)

Итак, вместо этого позвольте мне сузить область до этой более простой проблемы, которую я считаю выполнимой:

Учитывая кадр стека Python, какinspect.currentframe()найти начало стека оценки. В версии C структуры этоf_valuestack, Исходя из этого, нам нужен способ в Python считывать значения / объекты Python оттуда.

Обновите 2 хорошо, период времени для щедрости закончился, и никто (включая мой собственный краткий ответ) не предложил конкретный код. Я чувствую, что это хорошее начало, и теперь я понимаю ситуацию гораздо лучше, чем когда-либо. В обязательном "опишите, почему вы считаете, что должна быть щедрость", я перечислил один из предложенных вариантов "привлечь больше внимания к этой проблеме" и в той степени, в которой было что-то меньшее, чем дюжина взглядов на предыдущее воплощение проблема, поскольку я печатаю это, это было просмотрено чуть менее 190 раз. Так что это успех. Тем не мение...

Если кто-то в будущем решит продолжить это, свяжитесь со мной, и я назначу еще одну награду.

Спасибо всем.

5 ответов

Решение

Вот два возможных решения частичных решений, так как эта проблема не имеет простого очевидного ответа, за исключением:

  • модифицировать интерпретатор CPython или:
  • Оснащение байт-кодом до, например, через byteplay

Спасибо user2357112 за понимание проблемы и за описания:

  • различные стеки Python, используемые во время выполнения,
  • несмежный стек оценки,
  • кратковременность стека оценки и
  • верхний указатель стека, который живет только как локальная переменная C (которая во время выполнения может быть сохранена или может быть сохранена только в значении регистра).

Теперь о потенциальных решениях...

Первое решение - написать расширение C для доступа f_valuestack которая является нижней (не верхней) рамы. Из этого вы можете получить доступ к значениям, и это тоже должно идти в расширении C. Основная проблема здесь, поскольку это основание стека, состоит в том, чтобы понять, какая запись является вершиной или интересующей вас. Код записывает максимальную глубину стека в функции.

Расширение C обернет объект PyFrameObject, чтобы получить доступ к неэкспонированному полю. f_valuestack, Хотя PyFrameObject может измениться с версии Python на версию Python (поэтому расширению может потребоваться проверить, какая версия Python запущена), это все же выполнимо.

Исходя из этого, используйте абстрактную виртуальную машину, чтобы выяснить, в какой позиции вы бы находились для данного смещения, сохраненного в last_i,

Что-то похожее для моих целей было бы для моих целей использовать реальную, но альтернативную виртуальную машину, как, например, байтерн Нед Батчелдера. Он запускает интерпретатор байт-кода Python в Python. Преимущество здесь состоит в том, что, поскольку это действует как вторая виртуальная машина, хранилища не меняют работу текущей и реальной виртуальной машины CPython. Однако вам все равно придется иметь дело с фактом взаимодействия с внешним постоянным состоянием (например, вызовы через сокеты или изменения в файлах). И byterun необходимо будет расширить, чтобы охватить все коды операций и версии Python, которые потенциально могут понадобиться.

Кстати, для многоверсионного доступа к байт-коду единообразным способом (поскольку байт-код меняется не только немного, но также и набор подпрограмм для доступа к нему), см. Xdis.

Так что, хотя это не общее решение, оно, вероятно, может работать для особого случая, когда нужно выяснить значение EXEC вверх, которые кратко появляются в стеке оценки.

Это иногда возможно, с ctypes для прямого доступа к элементу структуры C, но это быстро запутывается.

Во-первых, нет общедоступного API для этого, на стороне C или на стороне Python, так что это не так. Придется копаться в недокументированных внутренностях реализации C. Я сосредоточусь на реализации CPython 3.6; детали должны быть похожими, хотя, вероятно, другими, в других версиях.

Структура PyFrameObject имеет член f_valuestack, который указывает на нижнюю часть его стека оценки. Он также имеет член f_stacktop, который указывает на вершину его стека оценки... иногда. Во время выполнения фрейма Python фактически отслеживает вершину стека, используя локальную переменную stack_pointer в _PyEval_EvalFrameDefault:

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

Как говорится в комментарии к коду, f_stacktop никогда не восстанавливается, если код во фрейме не выполняет yield... или await, который использует yield from реализация на уровне байт-кода. На yield или же awaitPython сохраняет stack_pointer обратно в f_stacktop, но кроме этого случая информация о том, где находится вершина стека, находится в локальной переменной уровня C, и к ней действительно трудно получить доступ.

когда f_stacktop не NULL, вы можете определить содержимое стека фрейма, изучив f_valuestack а также f_stacktop с типами. Вы даже можете получить расширенный набор содержимого стека без ctypes с помощью gc.get_referents(frame_object), хотя это будет содержать другие ссылки, которые не находятся в стеке фрейма.

когда f_stacktop NULL, определение содержимого стека фрейма практически невозможно. Вы все еще можете увидеть, где начинается стек f_valuestack, но вы не можете видеть, где это заканчивается.

  • Там код объекта кадра co_stacksize, который дает верхнюю границу размера стека, но не дает фактического размера стека.
  • Вы не можете сказать, где заканчивается стек, изучая сам стек, потому что Python не обнуляет указатели в стеке, когда он извлекает записи.
  • gc.get_referents не возвращает записи стека значений, когда f_stacktop нулевой. Он также не знает, как безопасно извлекать записи из стека в этом случае (и в этом нет необходимости, потому что если f_stacktop имеет нулевое значение и записи в стеке существуют, фрейм гарантированно достижим).
  • Вы могли бы быть в состоянии изучить кадр f_lasti чтобы определить последнюю инструкцию байт-кода, в которой она находилась, и попытаться выяснить, где эта инструкция покинет стек, но для этого потребуются глубокие знания байт-кода Python и цикла оценки байт-кода, и иногда это все еще неоднозначно. Это, по крайней мере, даст вам нижнюю границу текущего размера стека, что позволит вам безопасно проверить хотя бы некоторые из них.
  • Объекты фреймов имеют независимые стеки значений, которые не соприкасаются друг с другом, поэтому вы не можете посмотреть на дно стека одного фрейма, чтобы найти верх другого. (Стек значений фактически размещается внутри самого объекта фрейма.)
  • Вы могли бы выследить stackpointer локальная переменная с некоторой магией GDB или чем-то, но это было бы беспорядок.

Редактировать:

В Thonny IDE с открытым исходным кодом есть [sub] степенная оценка выражений. Посмотрите ответ автора на вопрос SO " Оценка трассировки Python" шаг за шагом.

Оригинальный ответ:

Ваша ссылка на отладчик Ruby говорит мне, что можно с уверенностью предположить, что вы хотите написать свой собственный отладчик Python?

Когда у вас есть кадр стека, он имеет такие свойства, как f_lineno который является текущим номером строки в исходном коде, f_locals для местных жителей в этом кадре, f_back для следующего внешнего кадра, f_code который является объектом кода, который, в свою очередь, дает вам доступ к байт-коду и т. д. - см. модуль проверки.

Вот еще немного информации о реализации вашего собственного отладчика. Цитата из моей магистерской диссертации, доступной онлайн: объединение обратной отладки и живого программирования с визуальным мышлением в компьютерном программировании:

Раздел 3.3.1

Большая часть CPython написана на языке программирования C, а файл исходного кода C (доступен по адресу http://github.com/python) для объекта frame, frame object.c, показывает, что только f_trace а также f_lineno можно установить. f_trace Атрибут может использоваться для указания перехвата трассировки или функции обратного вызова для определенных событий трассировки, которая позволяет создавать приложения, такие как профилировщики памяти, инструменты покрытия кода и регистраторы графа вызовов, такие как Pycallgraph4, как показано на рисунке 3.2. Он также используется для реализации отладчиков...

Раздел 3.8.1

Bdb - это базовый класс инфраструктуры отладчика в PSL (стандартная библиотека Python). Он предоставляет инфраструктуру отладки, регистрируя себя в интерпретаторе как ловушку трассировки, так что управление переходит к нему перед определенными событиями трассировки во время выполнения программы. Bdb содержит ряд абстрактных функций для реализации производными классами. На основе события трассировки производный класс может затем управлять тем, что должно произойти в этой точке, используя реализацию абстрактных функций. Если он возвращает управление интерпретатору, инструкция выполняется. Затем происходит следующее событие трассировки, и цикл повторяется.

Способ, которым BDB регистрирует себя как хук трассировки, заключается в settrace() функция PSL sys модуль. Это устанавливает f_trace атрибут стекового фрейма, так что функция bdb вызывается перед каждым событием трассировки в этом фрейме...

Pdb, другой класс PSL, расширяет класс bdb и реализует абстрактные функции. Также расширяя cmd Модуль PSL, являющийся платформой для линейно-ориентированных командных интерпретаторов, pdb обеспечивает взаимодействие с пользователем, предоставляя подсказку при выполнении условия остановки в терминале, в котором был запущен отладчик. Это делает стандартные команды отладки, такие как step, next а также continue, доступный пользователю. Предоставляя механизм для оценки произвольного кода Python в контексте кадра верхнего стека в любой точке, он позволяет исследовать конкретное поведение программы без изменения программного кода, что более подробно обсуждается в разделе 3.9.5.

Также см. Модуль трассировки Python и Модель данных, раздел внутренних типов.

Я не очень много работал с Python в течение нескольких лет, поэтому извиняюсь, если что-то не так. Кроме того, как вы могли заметить по ссылкам, это был Python 3.2.

Я написал код для этого. Кажется, это работает, поэтому я добавлю его к этому вопросу.

Как это сделать, разобрав инструкции и используя dis.stack_effectчтобы получить влияние каждой инструкции на глубину стека. Если есть прыжок, он устанавливает уровень стека на цель перехода.

Я думаю, что уровень стека детерминирован, т.е. он всегда один и тот же в любой заданной инструкции байт-кода в части кода, независимо от того, как он был достигнут. Таким образом, вы можете получить глубину стека для определенного байт-кода, непосредственно посмотрев на дизассемблирование байт-кода.

Есть небольшая загвоздка в том, что если вы находитесь в активном вызове, позиция кода отображается как последняя инструкция, являющаяся вызовом, но состояние стека на самом деле такое же, как до вызова. Это хорошо, потому что это означает, что вы можете воссоздать аргументы вызова из стека, но вы должны знать, что если инструкция является текущим вызовом, стек будет на уровне предыдущей инструкции.

Вот код из моего возобновляемого исключения, который делает это:

      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

Я пытался сделать это в этом пакете. Как отмечают другие, основная трудность заключается в определении вершины стека Python. Я пытаюсь сделать это с помощью эвристики, которую я здесь задокументировал.

Общая идея заключается в том, что к моменту вызова моей функции моментального снимка стек состоит из локальных переменных (как вы указываете), итераторов вложенных циклов for и любых триплетов исключений, которые в настоящее время обрабатываются. В Python 3.6 и 3.7 достаточно информации для восстановления этих состояний и, следовательно, вершины стека.

Я также полагался на совет пользователя2357112, чтобы проложить путь к реализации этой работы в Python 3.8.

Другие вопросы по тегам