Вы правы в том, что 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
- указатель на тело функции сопрограммы.
Эта функция выполняет следующие действия:
- выделить
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
, которая представляет экземпляр сопрограммы, и arg
будет передан в сопрограмму при возобновлении выполнения.
Эта функция вызывается вызывающим сопрограммой для передачи нового аргумента сопрограмме и его возобновления; возвращаемое значение этой функции - произвольная структура данных, возвращаемая (полученная) сопрограммой.
- сохранить все текущие флаги / регистры в
cs.caller_registers
, кроме RSP
, см. Шаг 3.
- хранить
arg
в cs.arg
- исправляет указатель стека invoker (
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
- исправьте указатель стека сопрограммы (
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_init
, а затем вы можете повторно вызывать экземпляр сопрограммы с помощью coroutine_next
.
Сама функция сопрограммы объявлена:
void my_coro(coroutine_state cs)
cs.initarg
содержит начальный аргумент функции (конструктор Think). Каждый раз, когда вызывается my_coro
, cs.arg
имеет другой аргумент, который был указан coroutine_next
. Вот как взаимодействующий с сопрограммой общается с сопрограммой. Наконец, каждый раз, когда сопрограмма хочет приостановить себя, она вызывает coroutine_yield
и передает ей один аргумент, который является возвращаемым значением для вызывающего сопрограммы.
Хорошо, теперь вы можете подумать «это просто!», Но я не учел все сложности загрузки регистров и флагов в правильном порядке, сохраняя при этом не поврежденный кадр стека и каким-то образом сохраняя адрес вашей структуры данных сопрограмм (вы просто переписали все свои регистры) в поточно-ориентированном виде. Для этого вам нужно выяснить, как работает ваш компилятор ... удачи:)