Ответ 1
Вы правы в том, что PUSHA
не работает на x64, он вызывает исключение #UD
, поскольку PUSHA
только толкает 16-разрядные или 32-разрядные регистры общего назначения. См. руководства Intel для всей информации, которую вы когда-либо хотели знать.
Настройка RIP
проста, jmp rax
установит RIP
в RAX
. Чтобы получить RIP, вы можете либо получить его во время компиляции, если уже знаете все исходные данные выхода coroutine, либо можете получить его во время выполнения, после этого вызова вы можете позвонить на следующий адрес. Вот так:
a:
call b
b:
pop rax
RAX
теперь будет b
. Это работает, потому что CALL
нажимает адрес следующей команды. Этот метод также работает и на IA32 (хотя я бы предположил, что лучше использовать его на x64, поскольку он поддерживает RIP-относительную адресацию, но я не знаю об этом). Конечно, если вы создаете функцию coroutine_yield
, она может просто перехватить адрес вызывающего абонента:)
Поскольку вы не можете вытолкнуть все регистры в стек в одной команде, я бы не рекомендовал хранить состояние сопрограммы в стеке, поскольку это все равно усложняет ситуацию. Я думаю, что лучше всего было бы выделить структуру данных для каждого экземпляра coroutine.
Почему вы обнуляете вещи в функции A
? Это, вероятно, не нужно.
Вот как бы я подошел ко всему, пытаясь сделать его максимально простым:
Создайте структуру coroutine_state
, которая содержит следующее:
-
initarg
-
arg
-
registers
(также содержит флаги) -
caller_registers
Создайте функцию:
coroutine_state* coroutine_init(void (*coro_func)(coroutine_state*), void* initarg);
где coro_func
является указателем на тело функции сопрограммы.
Эта функция выполняет следующие действия:
- выделить структуру
coroutine_state
cs
- присвойте
initarg
cs.initarg
, это будет начальный аргумент для сопрограммы - присвойте
coro_func
cs.registers.rip
- скопировать текущие флаги в
cs.registers
(не регистрировать, только флаги, так как нам нужны какие-то разумные флаги, чтобы предотвратить апокалипсис) - выделите некоторую приличную площадь для стека сопрограммы и назначьте ее
cs.registers.rsp
- возвращает указатель на выделенную структуру
coroutine_state
Теперь у нас есть другая функция:
void* coroutine_next(coroutine_state cs, void* arg)
где cs
- это структура, возвращаемая из coroutine_init
, которая представляет экземпляр coroutine, а arg
будет передаваться в сопрограмму coroutine по мере возобновления выполнения.
Эта функция вызывается сопроцессором coroutine для передачи некоторого нового аргумента в сопрограмму и возобновления ее, возвращаемое значение этой функции представляет собой произвольную структуру данных, возвращенную (предоставленную) сопрограммой.
- сохранить все текущие флаги/регистры в
cs.caller_registers
за исключениемRSP
, см. шаг 3. - сохраните
arg
вcs.arg
- исправить указатель стека указателя (
cs.caller_registers.rsp
), добавив2*sizeof(void*)
, исправит его, если вам повезет, вам придется посмотреть его, чтобы подтвердить его, вы, вероятно, хотите, чтобы эта функция была stdcall, поэтому никаких регистров подделаны, прежде чем называть его -
mov rax, [rsp]
, присвойтеRAX
cs.caller_registers.rip
; Объяснение: если ваш компилятор не находится на трещине,[RSP]
будет содержать указатель инструкции к инструкции, которая следует за инструкцией вызова, которая вызвала эту функцию (то есть: адрес возврата) - загрузите флаги и регистры из
cs.registers
-
jmp cs.registers.rip
, эффективно возобновление выполнения сопрограммы
Обратите внимание, что мы никогда не возвращаемся от этой функции, сопрограммы, которые мы переходим к "возвращает" для нас (см. coroutine_yield
). Также обратите внимание, что внутри этой функции вы можете столкнуться со многими сложностями, такими как пролог функций и эпилог, сгенерированный компилятором C, и, возможно, зарегистрировать аргументы, вы должны позаботиться обо всем этом. Как я уже сказал, stdcall избавит вас от многих неприятностей, я думаю, что gcc -fomit-frame_pointer удалит материал эпилога.
Последняя функция объявлена как:
void coroutine_yield(void* ret);
Эта функция вызывается внутри сопрограммы для "приостановки" выполнения сопрограммы и возврата к вызывающей стороне coroutine_next
.
- хранить флаги/регистры
in cs.registers
- исправить указатель стека coroutine (
cs.registers.rsp
), еще раз добавить2*sizeof(void*)
к нему, и вы хотите, чтобы эта функция также была stdcall -
mov rax, arg
(позволяет просто притворяться, что все функции в вашем компиляторе возвращают свои аргументы вRAX
) - загрузить флаги/регистры из
cs.caller_registers
-
jmp cs.caller_registers.rip
Это по существу возвращается из вызоваcoroutine_next
в стек стека сопроводителя сопроводителя, а так как возвращаемое значение передается вRAX
, мы вернулиarg
. Скажем, еслиarg
являетсяNULL
, то сопрограмма завершена, в противном случае это произвольная структура данных.
Итак, чтобы повторить, вы инициализируете сопрограмму coroutine с помощью coroutine_init
, затем вы можете повторно ссылаться на экземпляр сопроводителя с помощью coroutine_next
.
Сама функция сопроцессора объявлена:
void my_coro(coroutine_state cs)
cs.initarg
содержит аргумент начальной функции (конструктор мысли). Каждый раз, когда вызывается my_coro
, cs.arg
имеет другой аргумент, который был указан coroutine_next
. Таким образом, сопроцессор сопрограммы связывается с сопрограммой. Наконец, каждый раз, когда coroutine хочет приостановить себя, он вызывает coroutine_yield
и передает ему один аргумент, который является возвращаемым значением для сопроводителя сопрограммы.
Хорошо, теперь вы можете подумать: "Это просто!", но я оставил все сложности при загрузке регистров и флагов в правильном порядке, сохраняя при этом не поврежденный фрейм стека и каким-то образом сохраняя адрес вашей структуры данных coroutine (вы просто перезаписали все свои регистры) в потокобезопасной манере. Для этой части вам нужно будет узнать, как ваш компилятор работает внутри... удачи:)