Goアセンブラヌガむド



ランタむムを実装しお暙準ラむブラリを孊習する前に、抜象アセンブラGoをマスタヌする必芁がありたす。 このガむドが、必芁な知識をすばやく埗るのに圹立぀こずを願っおいたす。

内容


この蚘事は、読者があらゆる皮類のアセンブラヌの基本的な知識を持っおいるこずを前提ずしおいたす。

アヌキテクチャ関連の問題になるず、linux / amd64の䜿甚が垞に暗瀺されたす。

私たちは、垞に含たれるコンパむラの最適化を䜿甚したす。

すべおの匕甚は、特に明蚘しない限り、公匏文曞および/たたはコヌドベヌスからのものです。

疑䌌アセンブラヌ


Goコンパむラヌは、ハヌドりェアに瞛られない抜象的でポヌタブルなアセンブラヌを生成したす。 次に、アセンブラヌGoはこの擬䌌アセンブラヌを䜿甚しお、タヌゲット機噚甚のマシン固有の呜什を生成したす。

この远加の「レベル」には倚くの利点がありたす。 䞻なものは、Goを新しいアヌキテクチャに簡単に移怍できるこずです。 詳现に぀いおは、Rob Pikeのパフォヌマンス「 The Design of the Go Assembler 」をお送りしたす。

Goアセンブラに぀いお知っおおく必芁がある最も重芁なこずは、それが蚀語の基になるマシンを盎接衚珟しおいないこずです。 䜕かがマシンに盎接マッピングされ、䜕かはそうではありたせん。 実際、コンパむラはアセンブラを通垞のパむプラむンに枡す必芁はありたせん。 代わりに、コンパむラヌは、コヌドの生成埌に郚分的に遞択される呜什の半抜象セットで動䜜したす。 アセンブラは半抜象圢匏で動䜜するため、MOV呜什が衚瀺されおも、ツヌルキットがこの操䜜の移動呜什を生成するこずにはなりたせん。 おそらく、これはクリヌニングたたはロヌドの指瀺になりたす。 たたは、生成された呜什は、同じ名前のマシン呜什ず完党に䞀臎する堎合がありたす。 䞀般に、マシン固有の操䜜はそのように芋え、メモリの移動やルヌチンの呌び出しず戻りなどのより䞀般的な抂念はより抜象的です。 詳现はアヌキテクチャに䟝存し、䞍正確さをおforびしたす状況は䞍確かです。

アセンブラヌプログラムは、この半抜象呜什セットの説明を解析し、リンカヌに転送するための呜什に倉換する方法です。

単玔なプログラムの分解


次のGoコヌド direct_topfunc_call.go を怜蚎しおください。

//go:noinline func add(a, b int32) (int32, bool) { return a + b, true } func main() { add(10, 32) } 

コンパむラ指什に泚意しおください//go:noinline ...泚意しおください。

アセンブラヌでコヌドをコンパむルしたしょう

 $ GOOS=linux GOARCH=amd64 go tool compile -S direct_topfunc_call.go 0x0000 TEXT "".add(SB), NOSPLIT, $0-16 0x0000 FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB) 0x0000 FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 MOVL "".b+12(SP), AX 0x0004 MOVL "".a+8(SP), CX 0x0008 ADDL CX, AX 0x000a MOVL AX, "".~r2+16(SP) 0x000e MOVB $1, "".~r3+20(SP) 0x0013 RET 0x0000 TEXT "".main(SB), $24-0 ;; ...omitted stack-split prologue... 0x000f SUBQ $24, SP 0x0013 MOVQ BP, 16(SP) 0x0018 LEAQ 16(SP), BP 0x001d FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d MOVQ $137438953482, AX 0x0027 MOVQ AX, (SP) 0x002b PCDATA $0, $0 0x002b CALL "".add(SB) 0x0030 MOVQ 16(SP), BP 0x0035 ADDQ $24, SP 0x0039 RET ;; ...omitted stack-split epilogue... 

コンパむラの動䜜を理解するために、2぀の関数を行ごずに配眮したした。

add分析


 0x0000 TEXT "".add(SB), NOSPLIT, $0-16 


0x0000 FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
0x0000 FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)


FUNCDATAおよびPCDATAコンパむラヌによっお提䟛され、ガベヌゞコレクタヌの情報が含たれおいたす。

ただ深く掘り䞋げおはいけたせん。ガベヌゞコレクションを調べる蚘事でこれに戻りたす。

0x0000 MOVL "".b+12(SP), AX
0x0004 MOVL "".a+8(SP), CX


Go呌び出し芏玄は、呌び出し元のスタックのフレヌム内の事前予玄スペヌスを䜿甚しお、すべおの匕数をスタックにプッシュするように指瀺したす。 呌び出し元に匕数を枡し、呌び出し元に倀を返すこずができるように、スタックを瞮小および増加するこずは、呌び出し元の責任です。

GoコンパむラヌはPUSH / POPファミリヌ呜什を生成したせん。SP機噚の仮想スタックポむンタヌをデクリメントたたはむンクリメントするこずでスタックサむズが倉曎されたす 問題21に぀いおの説明を参照しおくださいSPレゞスタヌに぀いお 。

SP擬䌌レゞスタは、関数呌び出し甚に準備されたロヌカルフレヌム倉数ず匕数を参照するために䜿甚される仮想スタックポむンタヌです。 ロヌカルスタックフレヌムの先頭を指すため、リンクは[-framesize、0]の範囲の負のオフセットを䜿甚する必芁がありたす x-8(SP) 、 y-4(SP)など。

公匏ドキュメントでは、「すべおのナヌザヌ文字はFP疑䌌レゞスタヌ匕数およびロヌカル倉数に察するオフセットずしお曞き蟌たれたす」ず曞かれおいたすが、これはナヌザヌが䜜成したコヌドにのみ圓おはたりたす。

最新のコンパむラず同様に、生成されたコヌド内のGoツヌルキットは、垞にスタックポむンタヌからのオフセットを䜿甚しお匕数ずロヌカル倉数を盎接参照したす。 これにより、スタックフレヌムをレゞスタの少ないプラットフォヌムx86などで汎甚レゞスタずしお䜿甚できたす。

このような退屈な詳现が奜きな堎合は、「 x86-64スタックフレヌム図 」をご芧ください問題2の説明も参照しおくださいフレヌムポむンタヌ。

"".b+12(SP)および"".a+8(SP)は、スタックの最䞊郚から12および8バむトにあるアドレスを指したす芚えおおいおくださいスタックが䞋がっおいきたす。

.aおよび.bは、参照する堎所の任意の゚むリアスです。 セマンティックな意味はたったくありたせんが、仮想レゞスタぞの盞察アドレス指定を䜿甚する堎合に䜿甚するように芏定されおいたす。 以䞋は、仮想フレヌムポむンタヌに関するドキュメントの説明です。

FP擬䌌レゞスタは、関数の匕数を参照するために䜿甚される仮想フレヌムポむンタヌです。 コンパむラは仮想フレヌムポむンタをサポヌトし、スタック䞊の匕数を擬䌌レゞスタからのオフセットずしお参照したす。 したがっお、0FPは関数の最初の匕数、8FPは2番目64ビットマシン䞊などです。 ただし、この方法で関数の匕数を参照する堎合は、最初に名前を付ける必芁がありたす。たずえば、first_arg + 0FPおよびsecond_arg + 8FPここで、オフセット-フレヌムポむンタヌから-SBずは異なりたす。文字。 アセンブラはこの芏則を匷制的に䜿甚し、単玔な0FPず8FPを拒吊したす。 実際の名前は意味的には䞀臎したせんが、匕数の名前を文曞化するために䜿甚する必芁がありたす。

最埌に、泚意すべき他の2぀の重芁なポむント

  1. 最初の匕数aは0(SP)ではなく8(SP)にありたす。これは、呌び出し偎が疑䌌関数CALLを䜿甚しお、戻りアドレスを0(SP) CALL保存するためです。
  2. 匕数は逆の順序で枡されたす。 ぀たり、最初の匕数はスタックの最䞊郚に最も近くなりたす。

 0x0008 ADDL CX, AX 0x000a MOVL AX, "".~r2+16(SP) 0x000e MOVB $1, "".~r3+20(SP) 

ADDLは、 AXずCXにある2぀のロングワヌド4バむト倀などを远加し、結果はAX曞き蟌たれたす。 次に、この結果は"".~r2+16(SP)に移動され"".~r2+16(SP)このスタックでは、呌び出し元が以前に堎所を予玄し、そこで戻り倀を探したす。 繰り返したすが、この堎合は"".~r2には意味的な意味はありたせん。

Goが耇数の戻り倀を凊理する方法を瀺すために、 true定数ブヌル倀を返しtrue 。 メカニズムは、最初の戻り倀の堎合ずたったく同じですSP倉曎に察応するのはオフセットのみです。

 0x0013 RET 

RET疑䌌呜什は、呌び出しルヌチンから結果を正しく返すために、タヌゲットプラットフォヌムで䜿甚される呌び出し芏玄に必芁な呜什を挿入するようにアセンブラGoに指瀺したす。 これにより、コヌドは確実に0(SP)あるリタヌンアドレスをポップポップ0(SP) 、そこに戻りたす。

TEXTブロックの最埌の呜什は、䜕らかの遷移である必芁がありたす。これは通垞疑䌌RET呜什です。 そうでない堎合、リンカは、それ自䜓ぞの遷移jump-to-itselfを持぀呜什を远加したす。 TEXTブロックにフォヌルスルヌはありたせん。

䞀床に倚くの構文ずセマンティクスを孊ぶ必芁がありたす。 䞊蚘のむンラむンの抂芁は次のずおりです。

 ;; Declare global function symbol "".add (actually main.add once linked) ;; Do not insert stack-split preamble ;; 0 bytes of stack-frame, 16 bytes of arguments passed in ;; func add(a, b int32) (int32, bool) 0x0000 TEXT "".add(SB), NOSPLIT, $0-16 ;; ...omitted FUNCDATA stuff... 0x0000 MOVL "".b+12(SP), AX ;; move second Long-word (4B) argument from caller's stack-frame into AX 0x0004 MOVL "".a+8(SP), CX ;; move first Long-word (4B) argument from caller's stack-frame into CX 0x0008 ADDL CX, AX ;; compute AX=CX+AX 0x000a MOVL AX, "".~r2+16(SP) ;; move addition result (AX) into caller's stack-frame 0x000e MOVB $1, "".~r3+20(SP) ;; move `true` boolean (constant) into caller's stack-frame 0x0013 RET ;; jump to return address stored at 0(SP) 

そしお、 main.addはmain.add実行完了埌のスタックの内容の芖芚的衚珟です

  | +-------------------------+ <-- 32(SP) | | | G | | | R | | | O | | main.main's saved | W | | frame-pointer (BP) | S | |-------------------------| <-- 24(SP) | | [alignment] | D | | "".~r3 (bool) = 1/true | <-- 21(SP) O | |-------------------------| <-- 20(SP) W | | | N | | "".~r2 (int32) = 42 | W | |-------------------------| <-- 16(SP) A | | | R | | "".b (int32) = 32 | D | |-------------------------| <-- 12(SP) S | | | | | "".a (int32) = 10 | | |-------------------------| <-- 8(SP) | | | | | | | | | \ | / | return address to | \|/ | main.main + 0x30 | - +-------------------------+ <-- 0(SP) (TOP OF STACK) (diagram made with https://textik.com) 

main分析する


蚘事を読み進める必芁がないように、 main機胜がどのように芋えるかを思い出させおください。

 0x0000 TEXT "".main(SB), $24-0 ;; ...omitted stack-split prologue... 0x000f SUBQ $24, SP 0x0013 MOVQ BP, 16(SP) 0x0018 LEAQ 16(SP), BP ;; ...omitted FUNCDATA stuff... 0x001d MOVQ $137438953482, AX 0x0027 MOVQ AX, (SP) ;; ...omitted PCDATA stuff... 0x002b CALL "".add(SB) 0x0030 MOVQ 16(SP), BP 0x0035 ADDQ $24, SP 0x0039 RET ;; ...omitted stack-split epilogue... 0x0000 TEXT "".main(SB), $24-0 

新しいものはありたせん


0x000f SUBQ $24, SP
0x0013 MOVQ BP, 16(SP)
0x0018 LEAQ 16(SP), BP


䞊蚘のように、Goの呌び出し芏玄では、すべおの匕数をスタックに枡す必芁がありたす。

呌び出し元SUBQは、仮想スタックポむンタをデクリメントするこずにより、スタックフレヌムを24バむト増やしたす スタックが倧きくなるこずを忘れないでください。この堎合、 SUBQはスタックフレヌムを増やしたす 。 これらの24バむトの構成


最埌に、スタックを増やした埌、 LEAQはフレヌムポむンタヌの新しいアドレスを蚈算し、それをBP栌玍したす。

 0x001d MOVQ $137438953482, AX 0x0027 MOVQ AX, (SP) 

呌び出し元は、呌び出し先の匕数をクワッドワヌド8バむト倀ずしお受け取り、展開したばかりのスタックの䞀番䞊に眮きたす。

䞀芋するずランダムなゎミのように芋えたすが、実際には137438953482は4バむト倀10ず32に察応しおおり、これらは1぀の8バむト倀に接続されおいたす。

 $ echo 'obase=2;137438953482' | bc 10000000000000000000000000000000001010 \____/\______________________________/ 32 10 0x002b CALL "".add(SB) 

CALLを、静的ベヌスポむンタヌに察するオフセットずしおadd関数に適甚したす。 ぀たり、盎接アドレスぞの盎接遷移です。

CALLは、スタックの先頭に戻りアドレス8バむト倀も配眮するこずに泚意しおください。 したがっお、 add関数内からSPぞの各参照は8バむトシフトされたす たずえば、 "".aは0(SP)ではなく8(SP)たす。

 0x0030 MOVQ 16(SP), BP 0x0035 ADDQ $24, SP 0x0039 RET 

最埌に、我々

  1. フレヌムポむンタヌを1぀のスタックポむンタヌに巻き戻したす぀たり、1レベル「ダりン」したす。
  2. スタックを24バむト枛らしお、以前に占有しおいたスペヌスを返したす。
  3. アセンブラGoに戻りルヌチンを挿入するように䟝頌したす。

ゎルヌチン、スタック、パヌティションに関するいく぀かの蚀葉


今はゎルヌチンのギブルを扱う時でも堎所でもありたせんが、アセンブラヌに飛び蟌み始めた堎合、スタックの管理に関する指瀺にすぐに慣れる必芁がありたす。

これらのパタヌンを迅速に認識し、䞀般にそれらが䜕をどのように行うかを理解できる必芁がありたす。

スタック


Goプログラムのゎルヌチンの量は決定されおおらず、実際には数癟䞇に達する可胜性があるため、䜿甚可胜なメモリをすべお消費しないようにするには、実行時にゎルヌチンにスタックを割り圓おる保守的な方法に埓う必芁がありたす。

したがっお、新しいゎルヌチンはそれぞれ、実行時に最初に小さな2 KBスタックを取埗したす実際、ヒヌプ䞊にありたす。

実行䞭、goroutinはスタックの初期スペヌスより倧きくなるこずがありたす぀たり、スタックオヌバヌフロヌが発生したす。 これを防ぐために、ランタむム環境は、スタックを満たすずきに、叀いスタックの2倍のサむズの新しいスタックを割り圓お、その内容が新しいスタックにコピヌされたす。

このプロセスはスタックスプリットず呌ばれ、ゎルヌチンに動的なスタックメカニズムを提䟛したす。

郹門


スタック分離メカニズムが機胜するために、コンパむラヌは、スタックをオヌバヌフロヌさせる可胜性のある各関数の先頭ず末尟に新しい呜什を挿入したす。

䞍芁なオヌバヌヘッドを回避するために、スタックを超える可胜性が䜎い関数にはNOSPLITのマヌクが付けられたす。これは、チェックを挿入しないようコンパむラヌに指瀺したす。

メむン関数を芋おみたしょうが、今回はスタック分割プリアンブルを削陀せずに

 0x0000 TEXT "".main(SB), $24-0 ;; stack-split prologue 0x0000 MOVQ (TLS), CX 0x0009 CMPQ SP, 16(CX) 0x000d JLS 58 0x000f SUBQ $24, SP 0x0013 MOVQ BP, 16(SP) 0x0018 LEAQ 16(SP), BP ;; ...omitted FUNCDATA stuff... 0x001d MOVQ $137438953482, AX 0x0027 MOVQ AX, (SP) ;; ...omitted PCDATA stuff... 0x002b CALL "".add(SB) 0x0030 MOVQ 16(SP), BP 0x0035 ADDQ $24, SP 0x0039 RET ;; stack-split epilogue 0x003a NOP ;; ...omitted PCDATA stuff... 0x003a CALL runtime.morestack_noctxt(SB) 0x003f JMP 0 

ご芧のずおり、プリアンブルはプロロヌグず゚ピロヌグに分かれおいたす。


フィヌドバックルヌプが発生し、「飢ving」ゎルヌチンに十分に倧きなスタックが割り圓おられるたで機胜したす。

プロロヌグ

 0x0000 MOVQ (TLS), CX ;; store current *g in CX 0x0009 CMPQ SP, 16(CX) ;; compare SP and g.stackguard0 0x000d JLS 58 ;; jumps to 0x3a if SP <= g.stackguard0 

TLSは、ランタむム環境によっお維持される仮想レゞスタであり、珟圚のg 、぀たりゎルヌチンの状態党䜓を監芖するデヌタ構造ぞのポむンタヌを含みたす。

ランタむム゜ヌスコヌドのgの定矩を芋おみたしょう。

 type g struct { stack stack // 16 bytes // stackguard0 is the stack pointer compared in the Go stack growth prologue. // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption. stackguard0 uintptr stackguard1 uintptr // ...omitted dozens of fields... } 

16(CX)は、ランタむム環境でサポヌトされるしきい倀であるg.stackguard0に察応したす。 圌女はこの倀をスタックポむンタヌず比范し、ゎルヌチンがスタック䞍足に近づいおいるかどうかを調べたす。 ぀たり、プロロヌグは、珟圚のSP倀がstackguard0 正確には倧きいかどうかを確認し、必芁に応じお゚ピロヌグに進みたす。

゚ピロヌグ

 0x003a NOP 0x003a CALL runtime.morestack_noctxt(SB) 0x003f JMP 0 

゚ピロヌグの本䜓は単玔ですランタむム䞭に呌び出され、スタックを増やすためのすべおの䜜業を行った埌、関数の最初の呜什぀たり、プロロヌグに戻りたす。

NOP呜什はCALL前にあるため、プロロヌグはCALLに盎接移動したせん。 䞀郚のプラットフォヌムでは、これにより悪圱響が生じる可胜性がありたす。 そのため、コヌル自䜓の盎前に、通垞はnoop呜什を挿入し、 NOP到達したす 「問題4コヌル前のnop」段萜の説明も参照しおください。

埮劙なマむナス


氷山の䞀角だけを調べたした。 スタックの成長の内郚メカニズムにはさらに倚くのニュアンスがありたす。プロセスは非垞に耇雑で、詳现なレビュヌのために別の蚘事が必芁です。

おわりに


次の蚘事でGoデバむスに没頭するず、Goアセンブラヌは、内郚メカニズムず䞀芋あたり明らかではないものずの関係を理解するための最も重芁なツヌルの1぀になりたす。

参照資料


Source: https://habr.com/ru/post/J358088/


All Articles