12

私は楽しみと利益のためにコルーチンを実装しようと決心しました(私はそれを私がそれらと呼ぶべき方法だと思います)。アセンブラを使用する必要があると思います。これを実際に何かに役立てたい場合は、おそらく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

私が欲しいのは、スレッド化せずに、異なるスタックで2つの関数を実行する方法です。(明らかに、一度に実行されるのは1つだけです。スレッド化はありません。)これら2つの関数は、もう一方の実行を再開できる(そして自身の実行を停止できる)必要があります。彼らがlongjmp他の人と一緒にいた場合のように。他の関数に戻ると、元の場所(つまり、他の関数に制御を与えた呼び出し中または呼び出し後)から再開する必要があります。これは、にlongjmp戻る方法と少し似ていますsetjmp

これは私がそれを考えた方法です:

  1. 関数Aは、並列スタックを作成してゼロにします(メモリなどを割り当てます)。
  2. 関数Aは、すべてのレジスタを現在のスタックにプッシュします。
  3. 関数Aは、スタックポインターとベースポインターをその新しい場所に設定し、ジャンプバックする場所と命令ポインターを戻す場所を示す不思議なデータ構造をプッシュします。
  4. 関数Aはそのレジスタのほとんどをゼロにし、命令ポインタを関数の先頭に設定しますB

これは初期化用です。これで、次の状況が無期限にループします。

  1. 関数Bはそのスタックで機能し、必要な作業をすべて実行します。
  2. 機能は、中断して再び制御Bを与える必要があるポイントに到達します。A
  3. 関数Bは、すべてのレジスタをスタックにプッシュし、最初に与えられた不思議なデータ構造 を取得し、スタックポインタと命令ポインタを指示された場所に設定します。その過程で、再開する場所を指示する新しい変更されたデータ構造が返されます。AAAB
  4. 関数はウェイクアップし、スタックにプッシュしたすべてのレジスタをポップバックし、割り込みをかけて再び制御Aを与える必要があるポイントに到達するまで機能します。B

これはすべて私にはいいですね。しかし、私が完全に安心していないことがいくつかあります。

  • どうやら、古き良きx86には、pushaすべてのレジスタをスタックに送信するこの命令がありました。ただし、プロセッサアーキテクチャは進化しており、x86_64では、より多くの汎用レジスタと、おそらくいくつかのSSEレジスタがあります。pusha私は彼らを後押しする証拠を見つけることができませんでした。mordernx86CPUには約40個のパブリックレジスタがあります。私はすべてのことをpush自分でしなければなりませんか?さらに、pushSSEレジスターはありません(同等のものは必ずありますが、私はこの「x86アセンブラー」全体に慣れていません)。
  • 命令ポインタの変更は、言うのと同じくらい簡単ですか?mov rip, rax(Intel構文)のようにできますか?また、それから値を取得することは、それが絶えず変化するので、いくらか特別でなければなりません。私が好きならmov rax, rip(Intel構文も)、命令ripの上、movその後の命令、またはその間のどこかに配置されますか?ただjmp fooです。ダミー。
  • 不思議なデータ構造については何度か触れました。これまでは、ベースポインター、スタックポインター、および命令ポインターの少なくとも3つが含まれている必要があると想定していました。他に何かありますか?
  • 何か忘れましたか?
  • 私は物事がどのように機能するかを本当に理解したいのですが、それを実行するライブラリがいくつかあると確信しています。何でも知ってますか?スレッドのように、POSIXまたはBSDで定義された標準的な方法はありpthreadますか?

私の質問のテキストウォールを読んでくれてありがとう。

4

4 に答える 4

9

16ビットまたは32ビットの汎用レジスタのみPUSHAをプッシュするため、x64では動作しないという点で、例外が発生する#UDという点で正しいです。あなたが知りたいと思っていたすべての情報については、インテルのマニュアルを参照してください。PUSHA

設定RIPは簡単で、jmp raxに設定さRIPRAXます。RIP を取得するには、すべてのコルーチン出口オリジンが既にわかっている場合はコンパイル時に取得するか、実行時に取得することができます。その呼び出しの後に次のアドレスを呼び出すことができます。このような:

a:
call b
b:
pop rax

RAXになりますb。これCALLは、次の命令のアドレスをプッシュするために機能します。この手法は IA32 でも機能します (ただし、x64 では RIP 相対アドレス指定をサポートしているため、より適切な方法があると思いますが、私は知りません)。もちろん、関数を作成するcoroutine_yieldと、呼び出し元のアドレスを傍受することができます:)

1 つの命令ですべてのレジスタをスタックにプッシュすることはできないため、コルーチンの状態をスタックに格納することはお勧めしません。コルーチンのインスタンスごとにデータ構造を割り当てるのが最も良い方法だと思います。

function でゼロにするのはなぜ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. assign initargto 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)

wherecsは返される構造体coroutine_initで、コルーチン インスタンスを表し、arg実行を再開するときにコルーチンに供給されます。

この関数は、コルーチンの呼び出し元によって呼び出され、コルーチンに新しい引数を渡して再開します。この関数の戻り値は、コルーチンによって返される (生成される) 任意のデータ構造です。

  1. cs.caller_registersを除くすべての現在のフラグ/レジスタを保存しますRSP。手順 3 を参照してください。
  2. に収納argするcs.arg
  3. インボーカー スタック ポインター ( )を修正します。運が良ければcs.caller_registers.rsp追加すると修正されます。これを調べて確認する必要があります。おそらく、この関数を stdcall にして、呼び出す前にレジスタが改ざんされないようにする必要があります。2*sizeof(void*)
  4. mov rax, [rsp]、に割り当てRAXますcs.caller_registers.rip。説明: コンパイラがクラックされていない限り、[RSP]この関数を呼び出した call 命令の次の命令への命令ポインタ (つまり、戻りアドレス) を保持します。
  5. からフラグとレジスタをロードしますcs.registers
  6. jmp cs.registers.rip、コルーチンの実行を効果的に再開します

この関数から決して戻らないことに注意してください。ジャンプ先のコルーチンが「戻ります」( を参照coroutine_yield)。また、この関数内では、C コンパイラによって生成された関数のプロローグやエピローグなどの多くの複雑さに遭遇する可能性があり、おそらく引数を登録する可能性があることに注意してください。これらすべてを処理する必要があります。私が言ったように、stdcall は多くの手間を省きます。gcc の -fomit-frame_pointer はエピローグのものを取り除くと思います。

最後の関数は次のように宣言されます。

void coroutine_yield(void* ret);

この関数は、コルーチンの実行を「一時停止」して の呼び出し元に戻るために、コルーチン内で呼び出されcoroutine_nextます。

  1. フラグ/レジスタの保存in 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ました。である場合、コルーチンが終了したとしましょう。それ以外の場合は、任意のデータ構造ですargNULL

要約すると、 を使用してコルーチンを初期化しcoroutine_init、インスタンス化されたコルーチンを で繰り返し呼び出すことができますcoroutine_next

コルーチンの関数自体は次のように宣言されています。 void my_coro(coroutine_state cs)

cs.initarg関数の初期引数を保持します (コンストラクターと考えてください)。my_coroが呼び出されるたびcs.argに、 で指定された異なる引数を持ちますcoroutine_next。これは、コルーチン呼び出し元がコルーチンと通信する方法です。最後に、コルーチンがそれ自体を一時停止するたびに、 を呼び出しcoroutine_yield、1 つの引数を渡します。これは、コルーチン呼び出し元への戻り値です。

さて、「簡単だ!」と思うかもしれませんが、破損していないスタック フレームを維持し、コルーチン データ構造 (単にすべてのレジスタを上書きします)、スレッドセーフな方法で。その部分については、コンパイラが内部でどのように機能するかを調べる必要があります...頑張ってください:)

于 2010-06-22T05:38:20.763 に答える
1

Simon Tatham は、アーキテクチャ固有の知識やスタック操作を必要としない、C でのコルーチンの興味深い実装を行っています。それはまさにあなたが求めているものではありませんが、それでも少なくとも学術的な関心があるかもしれないと思いました.

于 2010-06-22T04:57:40.707 に答える
1

良い学習リファレンス: libcoroutine、特に setjmp/longjmp 実装。既存のライブラリを使用するのが楽しくないことはわかっていますが、少なくとも、どこに行くのかについての一般的な影響を得ることができます。

于 2010-06-22T02:30:37.140 に答える
-2

boost.org の boost.coroutine (boost.context) はすべてあなたのために行います

于 2013-03-11T12:10:19.227 に答える