Az x86 assembly nyelv egy alacsony szintű programozási nyelvcsalád, amelyet az x86 processzorosztályú tárgykódok létrehozására használnak. A nyelvcsalád korábbi fejlesztései visszafelé kompatibilitást biztosítottak a CPU-kkal, egészen az 1972 áprilisában bemutatott Intel 8008 mikroprocesszorig visszamenőleg. Assembly nyelvként szorosan kapcsolódnak az architektúra gépi kódú utasításaihoz, lehetővé téve a hardver feletti precíz vezérlést.
Az x86 assembly nyelvekben a mnemonikokat az alapvető CPU-utasítások ábrázolására használják, így a kód ember által olvashatóbb a nyers gépi kódhoz képest. Minden gépi kódú utasítás egy opkód, amelyet az assemblyben egy mnemonik helyettesít. Minden mnemonik a processzor által végrehajtott alapvető műveletnek felel meg, például aritmetikai számításoknak, adatmozgatásnak vagy vezérlési folyamattal kapcsolatos döntéseknek. Az assembly nyelveket leggyakrabban olyan alkalmazásokban használják, ahol a teljesítmény és a hatékonyság kritikus fontosságú. Ide tartoznak a valós idejű beágyazott rendszerek, az operációs rendszer kernelek és az eszközillesztők, amelyek mindegyike a hardvererőforrások közvetlen manipulálását igényelheti.
Ezenkívül a magas szintű programozási nyelvek fordítói néha assembly kódot generálnak köztes lépésként a fordítási folyamat során. Ez lehetővé teszi az assembly szintű optimalizálást, mielőtt előállítaná a processzor által végrehajtandó végső gépi kódot.
Az x86 assembly nyelv minden utasítását egy mnemonik képviseli, amely gyakran egy vagy több operandussal kombinálva egy vagy több bájttá, úgynevezett opkóddá alakítja át. Például a NOP utasítás a 0x90 opkóddá, a HLT utasítás pedig a 0xF4 hexadecimális opkóddá alakul. Léteznek olyan opkódok is, amelyekhez nem tartozik dokumentált mnemonik, és amelyeket a különböző processzorok eltérően értelmezhetnek. Az ilyen opkódok használata a program inkonzisztens viselkedését vagy akár kivételek generálását okozhatja egyes processzorokon.
Az x86 assembly nyelvnek két fő szintaxisa van: az Intel szintaxisa és az AT&T szintaxisa. Az Intel szintaxisa a DOS és Windows környezetekben domináns, míg az AT&T szintaxisa a Unix-szerű rendszerekben, mivel a Unixot eredetileg az AT&T Bell Labs fejlesztette ki. Az alábbiakban összefoglaljuk az Intel és az AT&T szintaxis közötti főbb különbségeket:
| AT&T | Intel | |
|---|---|---|
| Paraméter sorrend | movl $5, %eax
|
mov eax, 5
|
| Paraméter mérete | addl $0x24, %esp
movslq %ecx, %rax
paddd %xmm1, %xmm2
|
add esp, 24h
movsxd rax, ecx
paddd xmm2, xmm1
sub byte ptr [esi], 20h
A szélesség alapú nevek továbbra is megjelenhetnek az utasításokban, ha azok egy másik műveletet definiálnak.
|
| Jelek | A konstans értékek előtagja "$", a regisztereké "%". | Az assembler automatikusan felismeri a szimbólumok típusát; azaz, hogy regiszterek, konstansok vagy valami más. |
| Effektív címek | movl offset(%ebx, %ecx, 4), %eax
displacement(base, index, scale). |
mov eax, [ebx + ecx*4 + offset]
|
Sok x86 assembler Intel szintaxist használ, beleértve a FASM, MASM, NASM, TASM és YASM szintaxist. A GNU assembler, amely eredetileg AT&T szintaxist használt, a 2.10-es verzió óta mindkét szintaxist támogatja a .intel_syntax direktíván keresztül. Az AT&T x86 szintaxisának egy furcsasága, hogy az x87 lebegőpontos operandusok felcserélődnek, ami egy az eredeti AT&T assemblerből örökölt hiba.
Az AT&T szintaxisa szinte univerzális más architektúrákban (megtartva a mov utasítás operandussorrendjét); eredetileg PDP-11 assemblyhez tervezték, és átkerült a Unix-szerű rendszerekre. Ezzel szemben az Intel szintaxisa az x86 architektúrára jellemző, és az x86 platform hivatalos dokumentációjában is ezt használják. Az x86 architektúrát megelőző Intel 8080 szintén a "destination-first" („a cél az első”) sorrendet használja a mov utasításokhoz.
A legtöbb x86-os assembly nyelvben a foglalt szavak két részből állnak: mnemonikokból, amelyek opkódokká fordíthatók le, és direktívákból (vagy „pszeudo-opokból”), amelyek az assembler program olyan funkcióihoz férnek hozzá, amelyek túlmutatnak az opkódok egyszerű fordításán. Az előbbi rész listáját lásd az x86 utasításlistákban. Az utóbbi rész nagymértékben assembly-függő, az Intel-szintaxisú assemblerek között nincs szabvány. Az AT&T-szintaxisú assemblerek közös elnevezési módot használnak a direktívák esetében (minden direktíva ponttal kezdődik, mint például az .ascii), és számos alapvető direktíva, például az .ascii és a .string széles körben támogatott.
Az x86 processzorok olyan regiszterkészlettel rendelkeznek, amelyek bináris adatok és címek tárolására szolgálnak a program végrehajtása során. Ezeket a regisztereket általános célú regiszterekre, szegmensregiszterekre, utasításmutatóra, FLAGS regiszterre és a későbbi processzormodellekben bevezetett különféle bővítőregiszterekre osztják. Minden regiszter az általános képességein túl specifikus funkciókkal is rendelkezik:
Ezeknek a regisztereknek hagyományos szerepük van, de használatuk nincs szigorúan kikényszerítve. A programok általában szabadon használhatják őket más célokra.
XLAT-tal együtt használják.IN és OUT utasításokhoz.PUSH és POP műveletek során.MOVS (move string), az SI-t használják adatok memóriából való olvasására. A BX-hez hasonlóan indexelésre is használható. Hozzáadható a BP-hez vagy a BX-hez dupla indexeléshez.Az általános regiszterek mellett léteznek még a következők is:
Az x86 regisztereket a legtöbb utasítás használhatja. Például az Intel szintaxisában:
mov ax, 1234h ; copies the value 1234hex (4660d) into register AX
mov bx, ax ; copies the value of the AX register into the BX register
Az x86 architektúra valós és virtuális 8086 módban a szegmentálás néven ismert folyamatot használja a memória címzésére, nem pedig a sok más környezetben használt flat memória modellt. A szegmentálás egy memóriacím két részből, egy szegmensből (segment) és egy ofszetből (offset) való összeállítását jelenti; a szegmens egy 64 KiB (64×210) méretű címcsoport elejére mutat, és az ofszet határozza meg, hogy a kívánt cím milyen messze van ettől a kezdőcímtől. Szegmentált címzés esetén két regiszterre van szükség egy teljes memóriacímhez. Az egyik a szegmens, a másik az ofszet tárolására szolgál. Ahhoz, hogy vissza lehessen alakítani egy flat címre, a szegmens értékét négy bittel balra eltolják (egyenértékű a 24-el vagy 16-tal való szorzással), majd hozzáadják az ofszethez, így létrehozva a teljes címet, ami lehetővé teszi a 64k-os korlát áttörését az okos címválasztás révén, bár ez jelentősen bonyolultabbá teszi a programozást.
Valós módban, például, ha a DS a 0xDEAD hexadecimális számot, a DX pedig a 0xCAFE számot tartalmazza, akkor együttesen a 0xDEAD * 0x10 + 0xCAFE == 0xEB5CE memóriacímre mutatnának. Ezért a CPU valós módban akár 1 048 576 bájtot (1 MiB) tud megcímezni. A szegmens- és ofszetértékek kombinálásával egy 20-bites címet kapunk.
Az eredeti IBM PC 640 KB-ra korlátozta a programokat, de egy kibővített memóriaspecifikációval (Expanded memory) egy bankváltási sémát (Bank switching) valósítottak meg, amely használaton kívül esett, amikor a későbbi operációs rendszerek, például a Windows, az újabb processzorok nagyobb címtartományait használták, és saját virtuális memória-sémákat vezettek be.
A védett módot, az Intel 80286-tal kezdődően, az OS/2 is kihasználta. Számos hiányosság, például a BIOS elérésének lehetetlensége és a processzor újraindítása nélküli valós módba való visszatérés lehetetlensége megakadályozta a széles körű elterjedését. A 80286 továbbra is a memória 16-bites szegmensekben történő címzésére korlátozódott, ami azt jelentette, hogy egyszerre csak 216 bájt (64 kilobájt) érhető el. A 80286 kibővített funkcióinak eléréséhez az operációs rendszer védett módba állította a processzort, lehetővé téve a 24-bites címzést, és így 224 bájt (16 megabájt) memóriát.
Védett módban a szegmensválasztó három részre bontható: egy 13 bites indexre, egy táblajelző bitre (Table Indicator), amely meghatározza, hogy a bejegyzés a GDT-ben vagy az LDT-ben van-e, és egy 2 bites kért jogosultsági szintre (Requested Privilege Level); lásd x86 memória szegmentálás.
Amikor egy szegmenssel és eltolással rendelkező címre hivatkozunk, a segment:offset jelölést használjuk, így a fenti példában a 0xEB5CE flat address felírható 0xDEAD:0xCAFE-ként vagy szegmens és offszet regiszter párként; DS:DX.
A szegmens regiszterek és az általános regiszterek néhány speciális kombinációja fontos címekre mutat:
Az Intel 80386 három működési móddal rendelkezett: valós móddal, védett móddal és virtuális móddal. A 80286-ban debütált védett módot kibővítették, lehetővé téve a 80386 számára akár 4 GB memória címzését is. A vadonatúj virtuális 8086 mód (VM86) lehetővé tette egy vagy több valós módú program futtatását védett környezetben, amely nagyrészt emulálta a valós módot, bár egyes programok nem voltak kompatibilisek (jellemzően memória-címzési trükkök vagy meghatározatlan op-kódok használata miatt).
A 80386 kiterjesztett védett módjának 32 bites lapos memória modellje lehet a legfontosabb funkcióváltozás az x86 processzorcsaládban, amíg az AMD 2003-ban ki nem adta az x86-64-et, mivel ez segítette a Windows 3.1 (amely a védett módra támaszkodott) nagymértékű elterjedését, mivel a Windows mostantól sok alkalmazást, beleértve a DOS-alkalmazásokat is, futtathatott egyszerre a virtuális memória és az egyszerű multitasking használatával.
Az x86 processzorok ötféle működési módot támogatnak az x86 kódhoz: valós módot (Real Mode), védett módot (Protected Mode), hosszú módot (Long Mode), virtuális 86 módot (Virtual 86 Mode) és rendszerfelügyeleti módot (System Management Mode), amelyekben egyes utasítások elérhetők, mások nem. A 16 bites x86 processzorokon, amelyek a 8086, 8088, 80186, 80188 és 80286, az utasítások egy 16 bites részhalmaza érhető el. Ezek az utasítások valós módban minden x86 processzoron elérhetők, és 16 bites védett módban (80286-tól kezdődően) további, a védett módhoz kapcsolódó utasítások érhetők el. A 80386-os és újabb verziókon a 32 bites utasítások (beleértve a későbbi kiterjesztéseket is) szintén elérhetők minden módban, beleértve a valós módot is; ezeken a CPU-kon V86 mód és 32 bites védett mód is elérhető, további utasításokkal ezekben a módokban a funkcióik kezeléséhez. Az SMM, néhány saját speciális utasítással, néhány Intel i386SL, i486 és újabb CPU-n érhető el. Végül, hosszú módban (az AMD Opterontól felfelé) 64 bites utasítások és több regiszter is elérhető. Az utasításkészlet minden módban hasonló, de a memóriacímzés és a szóméret változó, ami eltérő programozási stratégiákat igényel.
Az x86 kód futtatásának módjai a következők:
A processzor bekapcsolás után azonnal valós módban fut, így egy operációs rendszer kernelének vagy más programnak explicit módon át kell váltania egy másik módba, ha nem valós módban kíván futni. Az üzemmódok közötti váltás a processzor vezérlőregisztereinek bizonyos bitjeinek módosításával történik némi előkészítés után, és a váltás után további beállításokra lehet szükség.
Egy régebbi BIOS-t futtató számítógépen a BIOS és a rendszerbetöltő valós módban fut. A 64 bites operációs rendszer kernelje ellenőrzi és hosszú módba kapcsolja a CPU-t, majd új kernel módú szálakat indít 64 bites kód futtatásával.
Egy UEFI-t futtató számítógépen az UEFI firmware (a CSM és a régebbi Option ROM kivételével), az UEFI rendszerbetöltő és az UEFI operációs rendszer kernelje is hosszú módban fut.
Általánosságban elmondható, hogy a modern x86 utasításkészlet jellemzői a következők:
ebp-ket számolva) általános regiszter 32 bites módban, és mind a tizenöt (rbp-ket számolva) általános regiszter 64 bites módban szabadon használható akkumulátorként vagy címzésre, a legtöbbjüket implicit módon bizonyos (többé-kevésbé) speciális utasítások is használják; az érintett regisztereket ezért ideiglenesen meg kell őrizni (általában vermelni), ha aktívak az ilyen utasítássorozatok során.xchg, cmpxchg/cmpxchg8b, xadd és egészértékű utasítások, amelyek a lock prefix-szel kombinálhatók).Az x86 architektúra hardveresen támogatja a hívási veremmechanizmust (call stack). Az olyan utasítások, mint a push, pop, call és ret, a megfelelően beállított veremmel együtt használatosak paraméterek átadására, a helyi adatok számára helyfoglalásra, valamint a hívás-visszatérési pontok mentésére és visszaállítására. A ret size utasítás nagyon hasznos a helytakarékos (és gyors) hívási konvenciók megvalósításához, ahol a hívott fél felelős a paraméterek által elfoglalt veremterület visszanyeréséért.
Amikor egy veremkeretet állítunk be egy rekurzív eljárás lokális adatainak tárolására, számos lehetőség közül választhatunk; a magas szintű enter utasítás (a 80186-ossal bevezetve) egy eljárás-beágyazási mélység argumentumot, valamint egy lokális méret argumentumot is fogad el, és gyorsabb lehet, mint a regiszterek explicitebb manipulálása (például push bp ; mov bp, sp ; sub sp, size). Az, hogy gyorsabb vagy lassabb, az adott x86-processzoros implementációtól, valamint a fordító, a programozó vagy az adott programkód által használt hívási konvenciótól függ; a legtöbb x86-os kódot különböző gyártók x86-processzorain és a processzorok különböző technológiai generációin való futtatásra tervezték, ami nagyon eltérő mikroarchitektúrákat és mikrokód-megoldásokat, valamint változó kapu- és tranzisztorszintű tervezési döntéseket von maga után.
A címzési módok teljes skálája (beleértve az „azonnali”, immediate és az „alap+eltolás”, base+offset módokat is), még olyan utasítások esetén is, mint a push és a pop, egyszerűvé teszi a verem közvetlen használatát az integer, lebegőpontos és cím adatokhoz, valamint az ABI specifikációit és mechanizmusait viszonylag egyszerűvé teszi egyes RISC architektúrákhoz képest (amelyek explicitebb call stack részleteket igényelnek).
Az x86 assembly tartalmazza a standard matematikai műveleteket: összeadás, add, sub, neg, imul és idiv (előjeles egész számokhoz), mul és div (előjel nélküli egész számokhoz); az and, or, xor és not logikai operátorokat; a biteltolásos aritmetikai és logikai műveleteket, sal/sar (előjeles egész számokhoz), shl/shr (előjel nélküli egész számokhoz); forgatást átvitellel és anélkül, rcl/rcr, rol/ror, valamint a BCD aritmetikai utasítások komplementerét, aaa, aad, daa. Az olyan utasítások, mint a cmp és a test, az operandusok módosítása nélkül állítják be a jelzőket.
Az x86 assembly nyelv utasításokat tartalmaz egy veremalapú lebegőpontos egységhez (FPU). Az FPU egy opcionális, különálló társprocesszor volt a 8086-tól a 80386-ig, chipre integrált opció volt a 80486 sorozathoz, és a Pentiummal kezdve minden Intel x86 CPU-ban szabványos funkció a 80486 óta. Az FPU utasításai közé tartozik az összeadás, kivonás, tagadás, szorzás, osztás, maradék, négyzetgyökvonás, egész csonkolás, tört csonkolás és a kettő hatványán való skálázás. A műveletek konverziós utasításokat is tartalmaznak, amelyek a következő formátumok bármelyikében képesek betölteni vagy tárolni egy értéket a memóriából: bináris kódolású decimális, 32 bites egész, 64 bites egész, 32 bites lebegőpontos, 64 bites lebegőpontos vagy 80 bites lebegőpontos (betöltéskor az érték az aktuálisan használt lebegőpontos módba konvertálódik). Az x86 számos transzcendens függvényt is tartalmaz, beleértve a szinusz, koszinusz, tangens, arkusztangens függvényt, a 2-es alapú hatványozást és a 2-es, 10-es vagy e-alapú logaritmusokat.
Az utasítások veremregiszterről veremregiszterre való formátuma általában fop st, st(n) vagy fop st(n), st, ahol st egyenértékű az st(0)-val, és st(n) a 8 veremregiszter egyike (st(0), st(1), ..., st(7)). Az egész számokhoz hasonlóan az első operandus egyben az első forrásoperandus és a céloperandus is. Az fsubr és az fdivr utasításokat kell kiemelni, mivel ezek cserélik fel először a forrásoperandusokat a kivonás vagy osztás végrehajtása előtt. Az összeadás, kivonás, szorzás, osztás, tárolás és összehasonlítás utasítások olyan utasításmódokat tartalmaznak, amelyek a műveletük befejezése után a verem tetejét veszik fel. Tehát például az faddp st(1), st végrehajtja az st(1) = st(1) + st(0) számítást, majd eltávolítja az st(0)-t a verem tetejéről, így az st(1)-ben kapott eredmény az st(0) verem tetejére kerül.
A modern x86-os CPU-k SIMD utasításokat tartalmaznak, amelyek nagyrészt ugyanazt a műveletet hajtják végre párhuzamosan számos, széles SIMD regiszterben kódolt értékkel. Különböző utasítástechnológiák támogatják a különböző műveleteket különböző regiszterkészleteken, de teljes egészében (az MMX-től az SSE4.2-ig) általános számításokat tartalmaznak egész vagy lebegőpontos aritmetikában (összeadás, kivonás, szorzás, eltolás, minimalizálás, maximalizálás, összehasonlítás, osztás vagy négyzetgyökvonás). Tehát például a paddw mm0, mm1 4 párhuzamos 16 bites (a w jelzi) egész értéket összead (a padd jelzi) mm0 értékekből mm1-hez, és az eredményt mm0-ban tárolja. A Streaming SIMD Extensions vagy SSE tartalmaz egy lebegőpontos módot is, amelyben a regiszterek legelső értékének módosítása történik meg (az SSE2-ben kibővítve). Néhány más szokatlan utasítást is hozzáadtak, beleértve az abszolút különbségek összegzését (a videotömörítés során a mozgásbecsléshez használják, például az MPEG-ben) és egy 16 bites szorzóösszesítő utasítást (hasznos szoftveralapú alfa-keveréshez és digitális szűréshez). Az SSE (az SSE3 óta) és a 3DNow! kiterjesztések összeadási és kivonási utasításokat tartalmaznak a párosított lebegőpontos értékek komplex számokhoz hasonlóan történő kezelésére.
Ezek az utasításkészletek számos fix részszavas utasítást is tartalmaznak az értékek regisztereken belüli keveréséhez, beszúrásához és kinyeréséhez. Ezenkívül vannak utasítások az adatok egész regiszterek és az XMM (SSE-ben használt)/FPU (MMX-ben használt) regiszterek közötti mozgatásához.
Az x86 processzor komplex címzési módokat is tartalmaz a memória azonnali eltolással történő címzéséhez, egy regiszterhez, egy eltolt regiszterhez, egy skálázott regiszterhez eltolással vagy anélkül, valamint egy opcionális eltolt regiszterhez és egy további skálázott regiszterhez. Így például a mov eax, [Table + ebx + esi*4] egyetlen utasításként kódolható, amely 32 bit adatot tölt be a ds szelektorból (Table + ebx + esi * 4) eltolásként kiszámított címről, és az eax regiszterbe tárolja. Általánosságban elmondható, hogy az x86 processzorok a működésük alatt álló regiszter méretéhez igazított memóriát tudnak betölteni és használni. (A SIMD utasítások félig betöltött utasításokat is tartalmaznak.)
A legtöbb 2 operandusú x86 utasítás, beleértve az egészértékű ALU utasításokat is, egy szabványos "címzési mód bájtot" használ, amelyet gyakran MOD-REG-R/M bájtnak neveznek. Sok 32 bites x86 utasításnak van egy SIB címzési mód bájtja is, amely a MOD-REG-R/M bájtot követi.
Elvileg, mivel az utasítás opkódja elkülönül a címzési mód bájtjától, ezek az utasítások ortogonálisak, mivel bármelyik opkód keverhető és illeszthető bármilyen címzési móddal. Az x86 utasításkészletet azonban általában nem ortogonálisnak tekintik, mivel a legtöbb diadikus művelet nem képes memóriáról memóriára működni, más opkódoknak van valamilyen rögzített címzési módjuk (nincs címzési mód bájtjuk), és minden regiszternek van egy preferált felhasználása.
Az x86 utasításkészlet karakterlánc betöltési, tárolási, mozgatási, szkennelési és összehasonlítási utasításokat (lods, stos, movs, scas és cmps) tartalmaz, amelyek minden műveletet egy megadott méretig hajtanak végre (b a 8 bites bájthoz, w a 16 bites szóhoz, d a 32 bites dupla szóhoz), majd növelik/csökkentik (a DF-től, az irányjelzőtől függően) az implicit címregisztert (si a lods-hoz, di a stos és scas-hoz, és mindkettő a movs és cmps-hez). A betöltési, tárolási és szkennelési műveletekhez az implicit cél/forrás/összehasonlító regiszter az al, ax vagy eax regiszterben található (a mérettől függően). A használt implicit szegmensregiszterek a ds az si-hez és az es a di-hez. A cx vagy ecx regisztert csökkenő számlálóként használják, és a művelet leáll, amikor a számláló eléri a nullát, vagy szkennelések és összehasonlítások esetén, amikor egyenlőséget vagy egyenlőtlenséget észlelnek. Sajnos az évek során ezen utasítások némelyikének teljesítménye elhanyagolódott, és bizonyos esetekben gyorsabb eredményeket lehet elérni elemibb utasításokkal történő kódolással. Az Intel és az AMD azonban frissített néhány utasítást, és 2025-től néhányuk nagyon tiszteletre méltó teljesítményt nyújt.
A verem egy memóriaterület és egy hozzá tartozó veremmutató (stack pointer), amely a verembe utoljára helyezett elemre mutat. A veremmutató értéke csökken az elemek hozzáadása előtt, push, és növekszik az elemek eltávolítása után, pop. 16 bites módban ez az implicit veremmutató SS:[SP], 32 bites módban SS:[ESP], 64 bites módban pedig [RSP] címmel rendelkezik. A veremmutató az utoljára tárolt értékre mutat, feltételezve, hogy a mérete megegyezik a processzor működési módjával (azaz 16, 32 vagy 64 bit), hogy megfeleljen a push/pop/call/ret utasítások alapértelmezett szélességének. Ide tartoznak az enter és leave utasítások is, amelyek adatokat foglalnak le és távolítanak el a verem tetejéről, miközben egy veremkeret-mutatót állítanak be a bp/ebp/rbp regiszterben. Azonban a közvetlen beállítás, vagyis az sp/esp/rsp regiszterhez való hozzáadás és kivonás is támogatott, így az enter/leave utasítások általában szükségtelenek.
Ez a kód egy tipikus magas szintű nyelvi függvény kezdete, amikor a fordítási optimalizálás ki van kapcsolva a hibakeresés megkönnyítése érdekében:
push rbp ; Save the calling function’s stack frame pointer (rbp register)
mov rbp, rsp ; Make a new stack frame below our caller’s stack
sub rsp, 32 ; Reserve 32 bytes of stack space for this function’s local variables.
; Local variables will be below rbp and can be referenced relative to rbp,
; again best for ease of debugging, but for best performance rbp will not
; be used at all, and local variables would be referenced relative to rsp
; because, apart from the code saving, rbp then is free for other uses.
… … ; However, if rbp is altered here, its value should be preserved for the caller.
mov [rbp-8], rdx ; Example of writing to a local variable (by its memory location) from register rdx
...funkcionálisan egyenértékű egyszerűen a következővel:
enter 32, 0
A verem manipulálására szolgáló egyéb utasítások közé tartozik a pushfd(32 bites) / pushfq(64 bites) és a popfd/popfq az EFLAGS (32 bites) / RFLAGS (64 bites) regiszter tárolásához és lekéréséhez.
A SIMD betöltés vagy tárolás értékei feltételezhetően a SIMD regiszter szomszédos pozícióiba vannak csomagolva, és szekvenciális little-endian sorrendben igazítják őket. Egyes SSE betöltési és tárolási utasítások megfelelő működéséhez 16 bájtos igazítás szükséges. A SIMD utasításkészletek tartalmaznak "prefetch" utasításokat is, amelyek végrehajtják a betöltést, de nem céloznak meg egyetlen regisztert sem, amelyeket a gyorsítótár betöltéséhez használnak. Az SSE utasításkészletek tartalmaznak nem időbeli tárolási utasításokat is, amelyek közvetlenül a memóriába mentik a mentéseket gyorsítótár-lefoglalás nélkül, ha a cél még nincs gyorsítótárazva (különben úgy fog viselkedni, mint egy normál tárolás).
A legtöbb általános egész és lebegőpontos (de nem SIMD) utasítás egy paramétert használhat komplex címként második forrásparaméterként. Az egész utasítások egy memóriaparamétert is elfogadhatnak céloperandusként.
Az x86 assembly rendelkezik egy feltétel nélküli ugrás művelettel, a jmp-vel, amely paraméterként azonnali címet, regisztert vagy indirekt címet vehet fel (vegye figyelembe, hogy a legtöbb RISC processzor csak linkregisztert vagy rövid azonnali elmozdulást támogat az ugráshoz).
Számos feltételes ugrás is támogatott, többek között a jz (jump on zero), jnz (jump on non-zero), jg (jump on greater than, signed), jl (jump on less than, signed), ja (jump on above/greater than, unsigned), jb (jump on below/less than, unsigned)), ja (ugrás felette/nagyobb, mint, előjel nélküli), jb (ugrás alatta/kisebb, mint, előjel nélküli). Ezek a feltételes műveletek az (E)FLAGS regiszterben lévő adott bitek állapotán alapulnak. Számos aritmetikai és logikai művelet állítja be, törli vagy kiegészíti ezeket a jelzőket az eredményüktől függően. Az összehasonlító cmp (compare) és test utasítások úgy állítják be a jelzőket, mintha kivonást, illetve bitenkénti ÉS műveletet végeztek volna, az operandusok értékének megváltoztatása nélkül. Léteznek olyan utasítások is, mint a clc (clear carry flag) és a cmc (complement carry flag), amelyek közvetlenül a jelzőkre hatnak. A lebegőpontos összehasonlításokat fcom vagy ficom utasításokon keresztül hajtják végre, amelyeket végül egészértékű jelzőkké kell konvertálni.
Minden ugrási műveletnek három különböző formája van, az operandus méretétől függően. A rövid ugrás (short jump) egy 8 bites előjeles operandust használ, amely relatív eltolás az aktuális utasításhoz képest. A közeli ugrás (near jump) hasonló a rövid ugráshoz, de 16 bites előjeles operandust használ (valós vagy védett módban) vagy 32 bites előjeles operandust (csak 32 bites védett módban). A távoli ugrás (far jump) az, amely a teljes szegmens bázis:eltolás értékét használja abszolút címként. Mindegyiknek létezik közvetett és indexelt formája is.
Az egyszerű ugrási műveleteken kívül léteznek a call (call a subroutine) és a ret (return from subroutine) utasítások. Mielőtt a vezérlés átadásra kerülne az alprogramnak, a call a call-ot követő utasítás eltolás címét a veremtárba helyezi; a ret ezt az értéket kiveszi a veremtárból, és ráugrik, gyakorlatilag visszaállítva a vezérlés folyamatát a programnak erre a részére. far call esetén a szegmens alapja kerül beírásra, majd az eltolás következik; a far ret kiveszi az eltolást, majd a szegmens alapját a visszatéréshez.
Két hasonló utasítás is létezik, az int (interrupt), amely elmenti az aktuális (E)FLAGS regiszter értékét a veremre, majd egy távoli hívást (far call) hajt végre, azzal a különbséggel, hogy cím helyett egy megszakításvektort (nterrupt vector), azaz a megszakításkezelők címeinek táblázatába mutató indexet használ. A megszakításkezelő jellemzően az összes többi általa használt CPU regisztert elmenti, kivéve, ha azokat egy művelet eredményének a hívó programnak való visszaadására használják (szoftverben ezt interruptnak nevezik). A megszakításutasítástól visszaérkező egyező érték az iret, amely a visszatérés után visszaállítja a jelzőket. A fent leírt típusú lágy megszakításokat (soft interrupt) egyes operációs rendszerek rendszerhívásokhoz használják, és kemény megszakításkezelők hibakereséséhez is használhatók. A kemény megszakításokat (hard interrupt) külső hardveresemények váltják ki, és meg kell őrizniük az összes regiszterértéket, mivel az aktuálisan futó program állapota ismeretlen. Védett módban az operációs rendszer beállíthatja a megszakításokat egy feladatváltás kiváltására, amely automatikusan menti az aktív feladat összes regiszterét.
A következő példák az úgynevezett Intel-szintaxis-ízt (Intel-syntax flavor) használják, ahogyan azt a Microsoft MASM, NASM és sok más assembler is használja. (Megjegyzés: Létezik egy alternatív AT&T-syntax flavor is, ahol a forrás- és céloperandusok sorrendje felcserélődik, sok más különbség mellett.)
A "Hello, world!" szöveg kiírható kevés operációs rendszer-segítséggel. A „Call outchr” egy olyan mechanizmust hív meg, amely egy AL-ben lévő karaktert ír ki a konzolra. A nullától eltérő hosszúságú karakterláncokat nulla bájttal kell lezárni. Megjegyzendő, hogy ez a példa egy 16 bites Intel 8086-ra vonatkozik.
hello:
mov si,msg ; address of string into SI
cld ; Clear direction to increment SI
lodsb ; Load first char in AL, inc SI
chrlp:
call outchr ; Print character in AL
lodsb ; Load next character in AL, inc SI
or al, al ; Is it a zero terminator?
jnz chrlp ; If not, continue
ret ; Return to caller
msg: db 'Hello, world!', 0xa, 0x0 ; string to be printed
A szoftveres megszakítás 21h utasítás használata az MS-DOS operációs rendszer meghívásához a kijelzőre történő kimenethez – más példák a libc C printf() rutinját használják a stdout-ra (szabványos kimenet) íráshoz. Megjegyzendő, hogy ez a példa 16 bites módot használ, mint egy Intel 8086. A következő példa egy Intel 386 kód 32 bites módban. A modern kód 64 bites módban lesz.
.model small
.stack 100h
.data
msg db 'Hello world!$'
.code
start:
mov ah, 09h ; Sets 8-bit register ‘ah’, the high byte of register ax, to 9, to
; select a sub-function number of an MS-DOS routine called below
; via the software interrupt int 21h to display a message
lea dx, msg ; Takes the address of msg, stores the address in 16-bit register dx
int 21h ; Various MS-DOS routines are callable by the software interrupt 21h
; Our required sub-function was set in register ah above
mov ax, 4C00h ; Sets register ax to the sub-function number for MS-DOS’s software
; interrupt int 21h for the service ‘terminate program’.
int 21h ; Calling this MS-DOS service never returns, as it ends the program.
end start
| ! MASM | NASM | Leírás |
|---|---|---|
; requires /coff switch on 6.15 and earlier versions
.386
.model small,c
.stack 1000h
|
; Image base = 0x00400000
%define RVA(x) (x-0x00400000)
|
Preambulum. A MASM megköveteli a címmodell és a veremméret definiálását. |
.data
msg db "Hello world!",0
|
section .data
msg db "Hello world!"
|
Adatok szakasz. A db (define byte) pszeudoopciót használjuk egy karakterlánc definiálására. |
.code
includelib libcmt.lib
includelib libvcruntime.lib
includelib libucrt.lib
includelib legacy_stdio_definitions.lib
extrn printf:near
extrn exit:near
public main
main proc
push offset msg
call printf
push 0
call exit
main endp
end
|
section .text
push dword msg
call dword [printf]
push byte +0
call dword [exit]
ret
section .idata
dd RVA(msvcrt_LookupTable)
dd -1
dd 0
dd RVA(msvcrt_string)
dd RVA(msvcrt_imports)
times 5 dd 0 ; ends the descriptor table
msvcrt_string dd "msvcrt.dll", 0
msvcrt_LookupTable:
dd RVA(msvcrt_printf)
dd RVA(msvcrt_exit)
dd 0
msvcrt_imports:
printf dd RVA(msvcrt_printf)
exit dd RVA(msvcrt_exit)
dd 0
msvcrt_printf:
dw 1
dw "printf", 0
msvcrt_exit:
dw 2
dw "exit", 0
dd 0
|
A kód (.text szakasz) és az import tábla. A NASM-ben az import tábla manuálisan jön létre, míg a MASM példában direktívák használatosak a folyamat egyszerűsítése érdekében. |
| AT&T (GNU as) | Intel (NASM) | Leírás |
|---|---|---|
.data
|
section .data
|
A Windows példához hasonlóan a .data fájl az inicializált adatok szakasza. |
str: .ascii "Hello, world!\n"
|
str: db 'Hello world!', 0Ah
|
Definiálj egy szöveges karakterláncot, amely tartalmazza a "Hello, world!" szöveget, majd egy új sort (\n, ami 0x0A). Kösd az "str" címkét a definiált karakterlánc címéhez. |
str_len = . - str
|
str_len: equ $ - str
|
Számítsd ki az str karakterlánc hosszát. A . jelentése "here" a gas nyelvben, a $ pedig ugyanezt jelenti a nasm nyelvben. A "here"-ből az "str" kivonásával megkapjuk az előzőleg definiált karakterlánc hosszát. |
.text
|
section .text
|
A Windows példához hasonlóan a .text a programkód szakasza. |
.globl _start
|
global _start
|
exportáld a _start függvényt globális hatókörbe, hogy a linker "láthassa" azt |
_start:
|
_start:
|
Definiáljunk egy _start nevű címkét, amelybe az alprogramunkat fogjuk írni. A _start név a Linux konvenció szerint a belépési pontot határozza meg. |
movl $4, %eax
movl $1, %ebx
movl $str, %ecx
movl $str_len, %edx
|
mov eax, 4
mov ebx, 1
mov ecx, str
mov edx, str_len
|
Készítsen elő egy rendszerhívást. Az EAX=4 a "sys_write" hívást kéri Linux x86 rendszeren. Az EBX=1 a sys_write esetében a "stdout" értéket jelenti. Az ECX a írandó karakterláncot, az EDX pedig a írandó bájtok számát tartalmazza. Ez egyenértékű a libc-vel csomagolt write(1, str, str_len) verzióval. |
int $0x80
|
int 80h
|
x86 rendszeren a "80h" rendszermegszakítást használják a rendszerhívás meghívására az eax, ebx, ecx és edx értékei alapján. |
movl $1, %eax
movl $0, %ebx
int $0x80
|
mov eax, 1
mov ebx, 0
int 80h
|
Tölts be egy másik rendszerhívást, majd hívd meg INT 80h paranccsal: EAX=1 = sys_exit, és az EBX for sys_exit tartalmazza a visszatérési értéket. A 0 visszatérési érték normál kilépést jelent. C szintaxisban az _exit(0);. |
Megjegyzés az NASM-hez:
; Ez a program 32 bites védett módban fut.
; build: nasm -f elf -F stabs name.asm
; link: ld -o name name.o
;
; 64 bites long módban használhatsz 64 bites regisztereket (pl. rax az eax helyett, rbx az ebx helyett stb.)
; A build parancsban a "-f elf" paramétert "-f elf64"-re kell cserélni.
; 64 bites long módban a "lea rcx, str" lenne az üzenet címe, jegyezd meg a 64 bites rcx regisztert.
;
; This program runs in 32-bit protected mode.
; gcc links the standard-C library by default
; build: nasm -f elf -F stabs name.asm
; link: gcc -o name name.o
;
; In 64-bit long mode you can use 64-bit registers (e.g. rax instead of eax, rbx instead of ebx, etc..)
; Also change "-f elf " for "-f elf64" in build command.
;
global main ; ‘main’ must be defined, as it being compiled
; against the C Standard Library
extern printf ; declares the use of external symbol, as printf
; printf is declared in a different object-module.
; The linker resolves this symbol later.
segment .data ; section for initialized data
string db 'Hello world!', 0Ah, 0 ; message string ending with a newline char (10
; decimal) and the zero byte ‘NUL’ terminator
; ‘string’ now refers to the starting address
; at which 'Hello, World' is stored.
segment .text
main:
push string ; Push the address of ‘string’ onto the stack.
; This reduces esp by 4 bytes before storing
; the 4-byte address ‘string’ into memory at
; the new esp, the new bottom of the stack.
; This will be an argument to printf()
call printf ; calls the C printf() function.
add esp, 4 ; Increases the stack-pointer by 4 to put it back
; to where it was before the ‘push’, which
; reduced it by 4 bytes.
ret ; Return to our caller.
Mivel a C futtatókörnyezetet használjuk, a main() függvényt a C futtatókörnyezet elvárásai szerint definiáljuk. Az exit meghívása helyett egyszerűen visszatérünk a main függvényből, hogy a futtatókörnyezet elvégezze a tisztítást.
Ez a példa modern 64 bites módban van.
; build: nasm -f elf64 -F dwarf hello.asm
; link: ld -o hello hello.o
DEFAULT REL ; use RIP-relative addressing modes by default, so [foo] = [rel foo]
SECTION .rodata ; read-only data should go in the .rodata section on GNU/Linux, like .rdata on Windows
Hello: db "Hello world!", 10 ; Ending with a byte 10 = newline (ASCII LF)
len_Hello: equ $-Hello ; Get NASM to calculate the length as an assembly-time constant
; the ‘$’ symbol means ‘here’. write() takes a length so that
; a zero-terminated C-style string isn't needed.
; It would be for C puts()
SECTION .text
global _start
_start:
mov eax, 1 ; __NR_write syscall number from Linux asm/unistd_64.h (x86_64)
mov edi, 1 ; int fd = STDOUT_FILENO
lea rsi, [rel Hello] ; x86-64 uses RIP-relative LEA to put static addresses into regs
mov rdx, len_Hello ; size_t count = len_Hello
syscall ; write(1, Hello, len_Hello); call into the kernel to actually do the system call
;; return value in RAX. RCX and R11 are also overwritten by syscall
mov eax, 60 ; __NR_exit call number (x86_64) is stored in register eax.
xor edi, edi ; This zeros edi and also rdi.
; This xor-self trick is the preferred common idiom for zeroing
; a register, and is always by far the fastest method.
; When a 32-bit value is stored into eg edx, the high bits 63:32 are
; automatically zeroed too in every case. This saves you having to set
; the bits with an extra instruction, as this is a case very commonly
; needed, for an entire 64-bit register to be filled with a 32-bit value.
; This sets our routine’s exit status = 0 (exit normally)
syscall ; _exit(0)
Az strace alatt futtatva ellenőrizhető, hogy a folyamat nem hajt végre extra rendszerhívásokat. A printf verzió sokkal több rendszerhívást kezdeményezne a libc inicializálásához és a dinamikus linkeléshez. De ez egy statikus futtatható fájl, mivel az ld kapcsolóval linkeltünk -pie vagy megosztott könyvtárak nélkül; a felhasználói térben csak a megadott utasítások futnak.
$ strace ./hello > /dev/null # without a redirect, your program's stdout is mixed with strace's logging on stderr. Which is normally fine
execve("./hello", ["./hello"], 0x7ffc8b0b3570 /* 51 vars */) = 0
write(1, "Hello world!\n", 13) = 13
exit(0) = ?
+++ exited with 0 +++
Az x86 architektúrában a flageket széles körben használják összehasonlításokhoz. Amikor két adat összehasonlításra kerül, a CPU beállítja a vonatkozó flaget vagy flageket. Ezt követően feltételes ugrási utasításokkal ellenőrizhetők a flagek, és elágazhatnak a futtatandó kódhoz, pl.:
cmp eax, ebx
jne do_something
; ...
do_something:
; do something here
Az összehasonlítási utasításokon kívül számos aritmetikai és egyéb utasítás létezik, amelyek biteket állítanak be a flags regiszterben. További példák a sub, test és add utasítások, és még sok más. Az olyan gyakori kombinációk, mint a cmp + feltételes ugrás, belsőleg "összeolvadnak" („makrofúzió”) egyetlen mikroutasítássá (μ-op), és gyorsak, feltéve, hogy a processzor kitalálja, merre fog menni a feltételes ugrás, jump vs. continue.
A flags regisztert az x86 architektúrában bizonyos funkciók vagy végrehajtási módok be- és kikapcsolására is használják. Például az összes maszkolható megszakítás letiltásához használhatja a következő utasítást:
cli
A flags regiszter közvetlenül is elérhető. A flags regiszter alsó 8 bitje betölthető az ah-ba a lahf utasítással. A teljes flags regiszter a pushfd/pushfq, popfd/popfq, int (beleértve az into utasítást is) és iret utasításokkal is áthelyezhető a verembe és a veremből.
Az x87 lebegőpontos matematikai alrendszernek saját független „flags” típusú regisztere is van, az fp állapotszó. Az 1990-es években nehézkes és lassú eljárás volt a regiszterben található flag bitek elérése, de a modern processzorokon léteznek „két lebegőpontos érték összehasonlítása” utasítások, amelyek közvetlenül használhatók a normál feltételes ugrás/elágazás utasításokkal, bármilyen közbeeső lépés nélkül.
Az utasításmutatót 16 bites módban ip-nek, 32 bites módban eip-nek, 64 bites módban pedig rip-nek nevezik. Az utasításmutató regiszter a processzor által végrehajtandó következő utasítás címére mutat. 16 bites vagy 32 bites módban nem érhető el közvetlenül, de az alábbihoz hasonló sorozat írható a next_line címének az eax regiszterbe való beillesztésére (32 bites kód):
call next_line
next_line:
pop eax
Az utasításmutatóba (programszámlálóba) írás egyszerű – egy jmp utasítás a megadott célcímet az utasításmutatóba tárolja, így például egy olyan sorozat, mint a következő, a rax tartalmát a rip-be (64 bites kód) helyezi:
jmp rax
64 bites módban az utasítások az utasításmutatóhoz képest relatív adatokra hivatkozhatnak, így kevésbé van szükség az utasításmutató értékének egy másik regiszterbe másolására.