Как я могу создать параллельный стек и выполнить сопрограмму на нем?

Я решил, что должен попытаться реализовать сопрограммы (я думаю, что это - то, как я должен назвать их) для забавы и прибыли. Я ожидаю должным быть использовать ассемблер и вероятно некоторый C, если я захочу сделать это на самом деле полезным для чего-нибудь.

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

Вы парни знаете setjmp и longjmp? Они позволяют Вам раскручивать стек до предопределенного местоположения и выполнение резюме оттуда. Однако это не может перемотаться к "позже" на стеке. Только возвратитесь ранее.

jmpbuf_t checkpoint;
int retval = setjmp(&checkpoint); // returns 0 the first time
/* lots of stuff, lots of calls, ... We're not even in the same frame anymore! */
longjmp(checkpoint, 0xcafebabe); // execution resumes where setjmp is, and now it returns 0xcafebabe instead of 0

То, что я хотел бы, является способом работать, без поточной обработки, двух функций на различных стеках. (Очевидно, только одно выполнение за один раз. Никакая поточная обработка, сказал я.) Эти две функции должны смочь возобновить выполнение других (и остановить их собственное). Несколько как то, если они были longjmpлуг к другому. После того как это возвращается к другой функции, это должно возобновить, где это уехало (то есть, во время или после вызова, который дал контроль к другой функции), немного как как longjmp возвраты к setjmp.

Это - то, как я думал это:

  1. Функция A создает и обнуляет параллельный стек (выделяет память и все это).
  2. Функция A нажатия все его регистры к текущему стеку.
  3. Функция A устанавливает указатель вершины стека и указатель базы к тому новому местоположению, и продвигает таинственную структуру данных, указывающую, где перейти назад и где задержать указатель команд.
  4. Функция A обнуляет большинство его регистров и устанавливает указатель команд на начало функции B.

Это для инициализации. Теперь, следующая ситуация неограниченно долго циклично выполнится:

  1. Функция B работы над тем стеком, делает любую работу, это должно.
  2. Функция B прибывает в точку, где она должна прервать и дать A управляйте снова.
  3. Функция B нажатия все его регистры к его стеку, берет таинственную структуру данных A дал его в самом начале и устанавливает указатель вершины стека и указатель команд туда, где A сказанный его. В процессе, это возвращает A новая, измененная структура данных, которая говорит, где возобновиться B.
  4. Функция A просыпается, выталкивая назад все регистры, которые это продвинуло к его стеку и действительно работает, пока это не прибывает в точку, где это должно прервать и дать B управляйте снова.

Все это звучит хорошим мне. Однако существует много вещей, с которыми я не точно непринужденно.

  • По-видимому, на старом добром x86, было это pusha инструкция, которая отправила бы все регистры в стек. Однако архитектуры процессора развиваются, и теперь с x86_64 у нас есть намного больше регистров общего назначения и вероятно несколько регистров SSE. Я не мог найти доказательство этим pusha действительно продвигает их. В ЦП mordern x86 существует приблизительно 40 общедоступных регистров. Сделайте я должен сделать весь pushes самостоятельно? Кроме того, существует нет push для регистров SSE (хотя там обязан быть эквивалентом — я плохо знаком с этим целым "x86 ассемблерная" вещь).
  • Действительно ли изменение является указателем команд как легким что это? Я могу сделать, как, mov rip, rax (Синтаксис Intel)? Кроме того, получение значения от него должно быть несколько особенным, поскольку оно постоянно изменяется. Если мне действительно нравится mov rax, rip (Синтаксис Intel снова), будет rip будьте расположены на mov инструкция, к инструкции после него, или где-нибудь между? Это справедливо jmp foo. Макет.
  • Я упомянул таинственную структуру данных несколько раз. До сих пор я предположил, что это должно содержать по крайней мере три вещи: указатель базы, указатель вершины стека и указатель команд. Есть ли что-либо еще?
  • Я забывал что-нибудь?
  • В то время как я действительно хотел бы понять, как вещи работают, я вполне уверен существует горстка библиотек, которые делают просто это. Вы знаете кого-либо? Есть ли любой POSIX - или BSD-определенный стандартный способ сделать это, как pthread для потоков?

Спасибо за чтение моего вопроса textwall.

11
задан zneak 25 October 2014 в 16:20
поделиться

3 ответа

Вы правы в том, что PUSHA не будет работать на x64, он вызовет исключение #UD , так как PUSHA только проталкивает 16-битные или 32-битные регистры общего назначения. См. руководства Intel для получения всей информации, которую вы когда-либо хотели знать.

Установить RIP просто: jmp rax установит RIP на RAX . Чтобы получить RIP, вы можете либо получить его во время компиляции, если вы уже знаете все источники выхода сопрограммы, либо вы можете получить его во время выполнения, вы можете позвонить по следующему адресу после этого вызова. Вот так:

a:
call b
b:
pop rax

RAX теперь будет b . Это работает, потому что CALL подталкивает адрес следующей инструкции. Этот метод работает и на IA32 (хотя я полагаю, что есть более удобный способ сделать это на x64, поскольку он поддерживает относительную адресацию RIP, но я не знаю ни одного).Конечно, если вы создадите функцию coroutine_yield , она сможет просто перехватить адрес вызывающего:)

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

Почему вы обнуляете вещи в функции A ? Вероятно, в этом нет необходимости.

Вот как я подхожу ко всему, пытаясь сделать это как можно проще:

Создайте структуру coroutine_state , которая содержит следующее:

  • initarg
  • arg
  • registers (также содержит флаги)
  • caller_registers

Создайте функцию:

coroutine_state * coroutine_init (void (* coro_func) (coroutine_state *), void * initarg);

где coro_func - указатель на тело функции сопрограммы.

Эта функция выполняет следующее:

  1. выделяет coroutine_state структуру cs
  2. присваивает initarg для cs.initarg , они будут быть начальным аргументом сопрограммы
  3. назначить coro_func для cs.registers.rip
  4. скопировать текущие флаги в cs.registers (не регистры, только флаги, поскольку нам нужны разумные флаги для предотвращения апокалипсиса)
  5. выделите некоторую область приличного размера для стека сопрограмм и назначьте ее для cs.registers.rsp
  6. возвращает указатель на выделенную структуру coroutine_state

Теперь у нас есть другая функция:

void * coroutine_next (coroutine_state cs, void * arg)

где cs - это структура, возвращенная из coroutine_init , которая представляет экземпляр сопрограммы, а arg будет загружен в сопрограмму, когда она возобновит выполнение.

Эта функция вызывается инициатором сопрограммы для передачи некоторого нового аргумента сопрограмме и возобновления ее работы, возвращаемое значение этой функции - это произвольная структура данных, возвращаемая (полученная) сопрограммой.

  1. сохранить все текущие флаги / регистры в cs.caller_registers за исключением RSP , см. Шаг 3.
  2. сохранить arg в cs .arg
  3. исправляет указатель стека вызывающего объекта ( cs.caller_registers.rsp ), добавление 2 * sizeof (void *) исправит это, если вам повезет, вы бы необходимо найти это, чтобы подтвердить это, вы, вероятно, хотите, чтобы эта функция была stdcall, чтобы никакие регистры не были подделаны перед ее вызовом
  4. mov rax, [rsp] , назначьте RAX на ] cs.caller_registers.rip ; объяснение: если ваш компилятор не работает в режиме взлома, [RSP] будет содержать указатель инструкции на инструкцию, которая следует за инструкцией вызова, которая вызвала эту функцию (то есть: адрес возврата)
  5. загрузить флаги и регистры из cs.registers
  6. jmp cs.registers.rip , эффективно возобновляя выполнение сопрограммы

Обратите внимание, что мы никогда не возвращаемся из этой функции, сопрограмма, к которой мы переходим, "возвращается" для нас (см. coroutine_yield ). Также обратите внимание, что внутри этой функции вы можете столкнуться со многими осложнениями, такими как пролог и эпилог функции, сгенерированные компилятором C, и, возможно, регистрировать аргументы, вы должны обо всем этом позаботиться. Как я уже сказал, stdcall избавит вас от множества неприятностей, я думаю, что gcc -fomit-frame_pointer удалит эпилог.

Последняя функция объявлена ​​как:

void coroutine_yield (void * ret);

Эта функция вызывается внутри сопрограммы, чтобы «приостановить» выполнение сопрограммы и вернуться к вызывающему coroutine_next .

  1. сохранить флаги / регистры в cs.registers
  2. исправить указатель стека сопрограмм ( cs.registers.rsp ), еще раз добавить 2 * sizeof (void *) , и вы хотите, чтобы эта функция также была stdcall
  3. mov rax, arg (давайте просто представим, что все функции в вашем компиляторе возвращают свои аргументы в RAX )
  4. загрузить флаги / регистры из cs.caller_registers
  5. jmp cs.caller_registers.rip По сути, это возвращается из вызова coroutine_next в кадре стека вызывающей сопрограммы, и поскольку возвращаемое значение передается в RAX мы вернули arg . Скажем так, если arg равно NULL , то сопрограмма завершена, в противном случае это произвольная структура данных.

Итак, напомним, вы инициализируете сопрограмму с помощью coroutine_init , затем вы можете повторно вызывать созданную сопрограмму с помощью coroutine_next .

Объявляется сама функция сопрограммы: void my_coro (coroutine_state cs)

cs.initarg содержит начальный аргумент функции (конструктор think). Каждый раз, когда вызывается my_coro , cs.arg имеет другой аргумент, указанный в coroutine_next . Вот как вызывающая сопрограмма взаимодействует с сопрограммой.Наконец, каждый раз, когда сопрограмма хочет приостановить себя, она вызывает coroutine_yield и передает ему один аргумент, который является возвращаемым значением вызывающей сопрограммы.

Хорошо, теперь вы можете подумать: «Все просто!», Но я упустил все сложности, связанные с загрузкой регистров и флагов в правильном порядке, при этом сохранив неповрежденный стековый фрейм и каким-то образом сохранив адрес структуры данных вашей сопрограммы. (вы просто перезаписали все свои регистры) потокобезопасным способом. Для этой части вам нужно будет узнать, как ваш компилятор работает внутри ... удачи :)

9
ответ дан 3 December 2019 в 10:03
поделиться

Хорошая обучающая ссылка: libcoroutine , особенно их реализация setjmp / longjmp. Я знаю, что использовать существующую библиотеку неинтересно, но вы можете, по крайней мере, получить общее представление о том, куда вы собираетесь.

1
ответ дан 3 December 2019 в 10:03
поделиться

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

1
ответ дан 3 December 2019 в 10:03
поделиться
Другие вопросы по тегам:

Похожие вопросы: