Я решил, что должен попытаться реализовать сопрограммы (я думаю, что это - то, как я должен назвать их) для забавы и прибыли. Я ожидаю должным быть использовать ассемблер и вероятно некоторый 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
.
Это - то, как я думал это:
A
создает и обнуляет параллельный стек (выделяет память и все это).A
нажатия все его регистры к текущему стеку.A
устанавливает указатель вершины стека и указатель базы к тому новому местоположению, и продвигает таинственную структуру данных, указывающую, где перейти назад и где задержать указатель команд.A
обнуляет большинство его регистров и устанавливает указатель команд на начало функции B
.Это для инициализации. Теперь, следующая ситуация неограниченно долго циклично выполнится:
B
работы над тем стеком, делает любую работу, это должно.B
прибывает в точку, где она должна прервать и дать A
управляйте снова.B
нажатия все его регистры к его стеку, берет таинственную структуру данных A
дал его в самом начале и устанавливает указатель вершины стека и указатель команд туда, где A
сказанный его. В процессе, это возвращает A
новая, измененная структура данных, которая говорит, где возобновиться B
.A
просыпается, выталкивая назад все регистры, которые это продвинуло к его стеку и действительно работает, пока это не прибывает в точку, где это должно прервать и дать B
управляйте снова.Все это звучит хорошим мне. Однако существует много вещей, с которыми я не точно непринужденно.
pusha
инструкция, которая отправила бы все регистры в стек. Однако архитектуры процессора развиваются, и теперь с x86_64 у нас есть намного больше регистров общего назначения и вероятно несколько регистров SSE. Я не мог найти доказательство этим pusha
действительно продвигает их. В ЦП mordern x86 существует приблизительно 40 общедоступных регистров. Сделайте я должен сделать весь push
es самостоятельно? Кроме того, существует нет push
для регистров SSE (хотя там обязан быть эквивалентом — я плохо знаком с этим целым "x86 ассемблерная" вещь).mov rip, rax
(Синтаксис Intel)? Кроме того, получение значения от него должно быть несколько особенным, поскольку оно постоянно изменяется. Если мне действительно нравится mov rax, rip
(Синтаксис Intel снова), будет rip
будьте расположены на mov
инструкция, к инструкции после него, или где-нибудь между?jmp foo
. Макет.pthread
для потоков?Спасибо за чтение моего вопроса textwall.
Вы правы в том, что 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
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
.
в 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
и передает ему один аргумент, который является возвращаемым значением вызывающей сопрограммы.
Хорошо, теперь вы можете подумать: «Все просто!», Но я упустил все сложности, связанные с загрузкой регистров и флагов в правильном порядке, при этом сохранив неповрежденный стековый фрейм и каким-то образом сохранив адрес структуры данных вашей сопрограммы. (вы просто перезаписали все свои регистры) потокобезопасным способом. Для этой части вам нужно будет узнать, как ваш компилятор работает внутри ... удачи :)
Хорошая обучающая ссылка: libcoroutine , особенно их реализация setjmp / longjmp. Я знаю, что использовать существующую библиотеку неинтересно, но вы можете, по крайней мере, получить общее представление о том, куда вы собираетесь.
Саймон Тэтхэм имеет интересную реализацию сопрограмм на C , которая не требует каких-либо специфических для архитектуры знаний или возни со стеком. Это не совсем то, что вам нужно, но я подумал, что, тем не менее, это может представлять по крайней мере академический интерес.