AMD ma specyfikację ABI, która opisuje konwencję wywoływania używaną na x86-64. Podążają za nią wszystkie systemy operacyjne, z wyjątkiem systemu Windows, który ma własną konwencję wywoływania x86-64. Czemu?
Czy ktoś zna techniczne, historyczne lub polityczne powody tej różnicy, czy też jest to wyłącznie kwestia zespołu NIH?
Rozumiem, że różne systemy operacyjne mogą mieć różne potrzeby w zakresie rzeczy wyższego poziomu, ale to nie wyjaśnia, dlaczego na przykład kolejność przekazywania parametrów rejestru w systemie Windows jest, rcx - rdx - r8 - r9 - rest on stack
podczas gdy wszyscy inni używają rdi - rsi - rdx - rcx - r8 - r9 - rest on stack
.
PS Zdaję sobie sprawę, jak ogólnie te konwencje wywoływania różnią się i wiem, gdzie znaleźć szczegóły, jeśli zajdzie taka potrzeba. Chcę wiedzieć, dlaczego .
Edytuj: aby dowiedzieć się, jak to zrobić, zobacz np. Wpis na Wikipedii i stamtąd linki.
źródło
Odpowiedzi:
Wybór czterech rejestrów argumentów na x64 - wspólny dla UN * X / Win64
Jedną z rzeczy, o których należy pamiętać w przypadku x86, jest to, że nazwa rejestru do kodowania „numeru rejestru” nie jest oczywista; pod względem kodowania instrukcji ( bajt MOD R / M , patrz http://www.c-jump.com/CIS77/CPU/x86/X77_0060_mod_reg_r_m_byte.htm ), numery rejestrów 0 ... 7 są - w tej kolejności -
?AX
,?CX
,?DX
,?BX
,?SP
,?BP
,?SI
,?DI
.Dlatego wybranie A / C / D (regs 0..2) jako wartości zwracanej i pierwszych dwóch argumentów (co jest "klasyczną"
__fastcall
konwencją 32-bitową ) jest logicznym wyborem. Jeśli chodzi o wersję 64-bitową, zamawiane są „wyższe” regy, a Microsoft i UN * X / Linux wybraliR8
/R9
jako pierwsi.Mając to na uwadze, wybór przez Microsoft
RAX
(wartości zwracanej) orazRCX
,RDX
,R8
,R9
(arg [0..3]) są zrozumiałe wybór jeśli zdecydujesz cztery rejestry argumentów.Nie wiem, dlaczego AMD64 UN * X ABI wybrało
RDX
wcześniejRCX
.Wybór sześciu rejestrów argumentowych na x64 - specyficzny dla UN * X
UN * X, na architekturach RISC, tradycyjnie przekazywał argumenty w rejestrach - konkretnie dla pierwszych sześciu argumentów (tak jest przynajmniej w przypadku PPC, SPARC, MIPS). Co może być jednym z głównych powodów, dla których projektanci AMD64 (UN * X) ABI zdecydowali się na użycie sześciu rejestrów również w tej architekturze.
Więc jeśli chcesz sześć rejestrów przekazać argumenty, i to jest logiczne, aby wybrać
RCX
,RDX
,R8
aR9
dla czterech z nich, co dwa pozostałe należy wybrać?„Wyższe” rejestry wymagają dodatkowego bajtu przedrostka instrukcji, aby je wybrać i dlatego mają większy rozmiar instrukcji, więc nie chciałbyś wybierać żadnej z nich, jeśli masz opcje. Z klasycznych rejestrów, ze względu na ukryte znaczenie
RBP
iRSP
nie są one dostępne, aRBX
tradycyjnie ma specjalne zastosowanie w UN * X (globalna tabela offsetów), z którymi najwyraźniej projektanci AMD64 ABI nie chcieli niepotrzebnie stać się niekompatybilnymi.Ergo, jedynym wyborem były
RSI
/RDI
.Więc jeśli musisz wziąć
RSI
/RDI
jako rejestry argumentów, jakie powinny to być argumenty?Wykonanie ich
arg[0]
iarg[1]
ma pewne zalety. Zobacz komentarz cHao.?SI
i?DI
są operandami źródłowymi / docelowymi instrukcji łańcuchowych, a jak wspomniało cHao, ich użycie jako rejestrów argumentów oznacza, że w przypadku konwencji wywoływania AMD64 UN * X najprostsza możliwastrcpy()
funkcja składa się na przykład tylko z dwóch instrukcji procesora,repz movsb; ret
ponieważ źródło / cel adresy zostały wprowadzone przez dzwoniącego do odpowiednich rejestrów. Występuje, szczególnie w niskopoziomowym i generowanym przez kompilator kodzie „klejowym” (pomyśl, na przykład, niektóre alokatory sterty w C ++ wypełniające zerami obiekty w konstrukcji lub strony jądra wypełniające stertysbrk()
lub błędy stronicowania przy kopiowaniu przy zapisie) ogromną ilość kopiowania / wypełniania bloków, dlatego będzie to przydatne w przypadku kodu tak często używanego do zapisywania dwóch lub trzech instrukcji procesora, które w przeciwnym razie ładowałyby takie argumenty adresu źródłowego / docelowego do „prawidłowe” rejestry.Więc w pewnym sensie, UN * X i Win64 różnią się tylko tym, że UN * X „wstawia” dwa dodatkowe argumenty, w celowo wybranych
RSI
/RDI
rejestrów, z naturalnym wyborem cztery argumentyRCX
,RDX
,R8
iR9
.Ponadto ...
Istnieje więcej różnic między interfejsami ABI UN * X i Windows x64 niż tylko mapowanie argumentów do określonych rejestrów. Aby zapoznać się z przeglądem systemu Win64, sprawdź:
http://msdn.microsoft.com/en-us/library/7kcdt6fy.aspx
Win64 i AMD64 UN * X również uderzająco różnią się sposobem wykorzystania przestrzeni stosowej; na przykład w Win64 wywołujący musi przydzielić przestrzeń stosu dla argumentów funkcji, mimo że argumenty 0 ... 3 są przekazywane w rejestrach. Z drugiej strony w UN * X funkcja liścia (tj. Taka, która nie wywołuje innych funkcji) nie jest nawet wymagana do przydzielania miejsca na stosie, jeśli nie potrzebuje więcej niż 128 bajtów (tak, posiadasz i możesz używać pewna ilość stosu bez przydzielania go ... no chyba, że jesteś kodem jądra, źródłem sprytnych błędów). Wszystko to są określone wybory optymalizacyjne, większość ich uzasadnienia jest wyjaśniona w pełnych odniesieniach ABI, na które wskazuje wikipedia pierwotnego postu.
źródło
__fastcall
są w 100% identyczne w przypadku, gdy mają nie więcej niż dwa argumenty nie większe niż 32-bitowe i zwracają wartość nie większą niż 32-bitowe. To nie jest mała klasa funkcji. Żadna taka wsteczna kompatybilność nie jest w ogóle możliwa między interfejsami UN * X ABI dla i386 / amd64.memcpy
że można to zaimplementować w ten sposób, a niestrcpy
.IDK, dlaczego Windows zrobił to, co zrobił. Aby zgadnąć, zobacz koniec tej odpowiedzi. Byłem ciekawy, jak zdecydowano o konwencji wywoływania SysV, więc poszperałem w archiwum list dyskusyjnych i znalazłem kilka fajnych rzeczy.
Warto przeczytać niektóre z tych starych wątków na liście mailingowej AMD64, ponieważ architekci AMD byli na niej aktywni. Np. wybór nazw rejestrów był jedną z najtrudniejszych
UAX
części: AMD rozważało zmianę nazwy oryginalnych 8 rejestrów r0-r7 lub wywoływanie nowych rejestrów w stylu .Ponadto, informacje zwrotne od jądra DEVS zidentyfikowanych rzeczy, które sprawiły, że oryginalny projekt
syscall
iswapgs
bezużyteczny . W ten sposób AMD zaktualizowało instrukcję, aby rozwiązać ten problem przed wypuszczeniem jakichkolwiek rzeczywistych układów. Ciekawe jest również to, że pod koniec 2000 roku założono, że Intel prawdopodobnie nie przyjmie AMD64.Konwencja wywołań SysV (Linux) i decyzja o tym, ile rejestrów powinno być zachowanych w porównaniu z zapisywaniem wywołań, została podjęta początkowo w listopadzie 2000 r. Przez Jana Hubickiego (programistę gcc). On skompilowany SPEC2000 i spojrzał na rozmiar kodu i liczby instrukcji. Ten wątek dyskusji odbija się wokół niektórych z tych samych pomysłów, co odpowiedzi i komentarze na to pytanie SO. W drugim wątku zaproponował obecną sekwencję jako optymalną i miejmy nadzieję ostateczną, generującą mniejszy kod niż niektóre alternatywy .
Używa terminu „globalny” na oznaczenie rejestrów z zachowaniem wywołań, które muszą być wypychane / popychane, jeśli są używane.
Wybór
rdi
,rsi
,rdx
jak pierwsze trzy args było motywowane przez:memset
lub inną funkcję ciągu C w swoich argumentach (gdzie gcc wstawia operację ciągu rep?)rbx
jest zachowywany, ponieważ posiadanie dwóch rejestrów zachowanych w wywołaniach dostępnych bez prefiksów REX (rbx i rbp) jest wygraną. Prawdopodobnie wybrany, ponieważ jest to jedyny inny reg, który nie jest domyślnie używany przez żadną instrukcję. (ciąg powtórzeń, liczba przesunięć i wyjścia / wejścia mul / div dotykają wszystkiego innego).(tło:
syscall
/sysret
nieuchronnie niszczyrcx
(zrip
) ir11
(zRFLAGS
), więc jądro nie może zobaczyć, co było pierwotnie wrcx
czasie działaniasyscall
.)Wywołanie systemowe jądra ABI zostało wybrane tak, aby pasowało do wywołania funkcji ABI, z wyjątkiem
r10
zamiast zamiastrcx
, więc opakowanie libc działa jakmmap(2)
can po prostumov %rcx, %r10
/mov $0x9, %eax
/syscall
.Zauważ, że konwencja wywoływania SysV używana przez i386 Linux jest do bani w porównaniu do 32-bitowego __vectorcall systemu Windows. Przekazuje wszystko ze stosu i wraca tylko
edx:eax
do int64, a nie do małych struktur . Nic dziwnego, że podjęto niewielki wysiłek, aby zachować zgodność z nim. Kiedy nie ma powodu,rbx
by tego nie robić , robili takie rzeczy, jak zachowywanie połączeń, ponieważ zdecydowali, że posiadanie innego w oryginalnym 8 (który nie potrzebuje przedrostka REX) było dobre.Ustalenie optymalnego wskaźnika ABI jest o wiele ważniejsze w perspektywie długoterminowej niż jakiekolwiek inne rozważanie. Myślę, że wykonali całkiem dobrą robotę. Nie jestem do końca pewien, czy zwracać struktury spakowane do rejestrów, zamiast różnych pól w różnych rejestrach. Wydaje mi się, że kod, który przekazuje je według wartości bez faktycznego działania na polach, wygrywa w ten sposób, ale dodatkowa praca związana z rozpakowaniem wydaje się głupia. Mogli mieć więcej rejestrów zwracających liczby całkowite, więcej niż tylko
rdx:rax
, więc zwrócenie struktury z 4 członami może zwrócić je w postaci rdi, rsi, rdx, rax lub coś w tym rodzaju.Rozważali przekazywanie liczb całkowitych w regach wektora, ponieważ SSE2 może operować na liczbach całkowitych. Na szczęście tego nie zrobili. Liczby całkowite są bardzo często używane jako przesunięcia wskaźnika, a podróż w obie strony do pamięci stosowej jest dość tania . Również instrukcje SSE2 zajmują więcej bajtów kodu niż instrukcje w postaci liczb całkowitych.
Podejrzewam, że projektanci Windows ABI mogli dążyć do zminimalizowania różnic między 32 a 64 bitami z korzyścią dla ludzi, którzy muszą przenosić asm z jednego na drugi lub mogą używać kilku
#ifdef
s w niektórych ASM, aby to samo źródło mogło łatwiej budować 32- lub 64-bitowa wersja funkcji.Minimalizowanie zmian w łańcuchu narzędzi wydaje się mało prawdopodobne. Kompilator x86-64 wymaga oddzielnej tabeli, w której rejestr jest używany do czego i jaka jest konwencja wywoływania. Niewielkie nakładanie się na wersję 32-bitową prawdopodobnie nie przyniesie znaczących oszczędności w rozmiarze / złożoności kodu łańcucha narzędzi.
źródło
Pamiętaj, że Microsoft był początkowo „oficjalnie niezobowiązujący w stosunku do wczesnych wysiłków AMD64” (z „A History of Modern 64-bit Computing” autorstwa Matthew Kernera i Neila Padgetta), ponieważ byli silnymi partnerami Intela w zakresie architektury IA64. Myślę, że oznaczało to, że nawet gdyby w innym przypadku byliby otwarci na współpracę z inżynierami GCC nad ABI do użytku zarówno w systemie Unix, jak i Windows, nie zrobiliby tego, ponieważ oznaczałoby to publiczne wspieranie wysiłków AMD64, kiedy tego nie zrobili. t jeszcze oficjalnie to zrobił (i prawdopodobnie zdenerwowałby Intela).
Co więcej, w tamtych czasach Microsoft nie miał absolutnie żadnych skłonności do przyjaźni z projektami open source. Na pewno nie Linux czy GCC.
Dlaczego więc mieliby współpracować przy ABI? Domyślam się, że ABI są różne po prostu dlatego, że zostały zaprojektowane mniej więcej w tym samym czasie i w izolacji.
Inny cytat z „A History of Modern 64-bit Computing”:
To wskazuje, że nawet AMD nie uważało, że współpraca między MS i Unixem była najważniejsza, ale posiadanie obsługi Unix / Linux było bardzo ważne. Może nawet próba przekonania jednej lub obu stron do kompromisu lub współpracy nie była warta wysiłku ani ryzyka (?) Irytowania którejkolwiek z nich? Być może AMD pomyślało, że nawet sugerowanie wspólnego ABI może opóźnić lub zakłócić ważniejszy cel, jakim jest po prostu przygotowanie obsługi oprogramowania, gdy chip będzie gotowy.
Spekulacje z mojej strony, ale myślę, że głównym powodem, dla którego ABI są różne, był polityczny powód, dla którego MS i strony Unix / Linux po prostu nie współpracowały nad tym, a AMD nie postrzegało tego jako problemu.
źródło
__vectorcall
ponieważ przekazywanie__m128
stosu było do niczego. Mając semantykę połączeń zachowane za niską 128b niektórych regs wektor jest również dziwny (częściowo wina Intela na nie projektuje rozszerzalną zapisu / przywrócenia mechanizmu z SSE pierwotnie, i nadal nie z AVX.)alloca
lub kilku innych przypadków). Jest to normalne, jeśli jesteś przyzwyczajony dogcc -fomit-frame-pointer
bycia domyślnym w systemie Linux. ABI definiuje metadane, które pozwalają na obsługę wyjątków. (Zakładam, że działa to coś w rodzaju pliku CFI systemu V w systemie GNU / Linux x86-64.eh_frame
).gcc -fomit-frame-pointer
jest ustawieniem domyślnym (z włączoną optymalizacją) od zawsze na x86-64, a inne kompilatory (takie jak MSVC) robią to samo.Win32 ma swoje własne zastosowania dla ESI i EDI i wymaga, aby nie były modyfikowane (lub przynajmniej aby zostały przywrócone przed wywołaniem do API). Wyobrażam sobie, że 64-bitowy kod robi to samo z RSI i RDI, co wyjaśniałoby, dlaczego nie są one używane do przekazywania argumentów funkcji.
Nie potrafię jednak powiedzieć, dlaczego zamieniono RCX i RDX.
źródło
__fastcall
konwencję wywoływania. Twierdzisz, że Win32 / Win64 nie są kompatybilne, ale przyjrzyj się uważnie: w przypadku funkcji, która pobiera dwa 32-bitowe argumenty i zwraca 32-bitowe, Win64 i Win32 są w__fastcall
rzeczywistości w 100% kompatybilne (te same reguły dotyczące przekazywania dwóch 32-bitowych argumentów, ta sama wartość zwracana). Nawet kod binarny (!) Może działać w obu trybach pracy. Strona UNIX całkowicie zerwała ze „starymi sposobami”. Nie bez powodu, ale przerwa to przerwa.