GNU Assembler: Unterschied zwischen den Versionen
K →Makros |
KKeine Bearbeitungszusammenfassung |
||
| Zeile 1: | Zeile 1: | ||
== GNU Assembler == | == GNU Assembler == | ||
Der GNU Assembler, häufig als GAS bezeichnet, ist ein Teil der GNU Binutils und ein wesentlicher Bestandteil der GNU Compiler Collection (GCC). Hier sind einige wichtige Punkte über den GNU Assembler: | |||
#Zweck und Funktion: GAS ist ein Assembler, der Quellcode in Assemblersprache in Maschinencode übersetzt. Dies ist ein wichtiger Schritt im Kompilierungsprozess, insbesondere bei der Umwandlung von Hochsprachen wie C oder C++ in ausführbare Programme. | |||
#Unterstützte Architekturen: GAS unterstützt eine Vielzahl von Prozessorarchitekturen, darunter x86, ARM, MIPS, PowerPC und viele mehr. Diese Vielseitigkeit macht es zu einem äußerst nützlichen Werkzeug für die Entwicklung auf verschiedenen Plattformen. | |||
#Syntax: GAS verwendet eine spezielle Syntax, die als AT&T-Syntax bekannt ist. Diese unterscheidet sich von der Intel-Syntax, die in vielen anderen Assemblern verwendet wird. Zum Beispiel verwendet GAS das Format opcode source, destination, während die Intel-Syntax opcode destination, source verwendet. | |||
#Integration mit GCC: Da GAS ein Teil der GNU Binutils ist, arbeitet es nahtlos mit GCC zusammen. Wenn Sie ein Programm mit GCC kompilieren, wird der Quellcode in der Regel zuerst in Assemblercode und dann in Maschinencode umgewandelt, wobei GAS diese Umwandlung übernimmt. | |||
#Befehlszeilenoptionen: GAS bietet eine Vielzahl von Befehlszeilenoptionen, die es Entwicklern ermöglichen, die Assemblierung fein abzustimmen. Einige gängige Optionen sind -o zum Festlegen des Ausgabedateinamens und -g zum Hinzufügen von Debugging-Informationen. | |||
#Lizenz: GAS wird unter der GNU General Public License (GPL) veröffentlicht. Dies bedeutet, dass es frei verfügbar ist und jeder den Quellcode einsehen, modifizieren und weiterverbreiten kann, solange die Bedingungen der GPL eingehalten werden. | |||
=== Präprozessor-Direktiven === | === Präprozessor-Direktiven === | ||
| Zeile 196: | Zeile 206: | ||
| ǀǀ || Logisches ODER | | ǀǀ || Logisches ODER | ||
|} | |} | ||
=== C to Assembly === | === C to Assembly === | ||
Version vom 13. September 2024, 09:29 Uhr
GNU Assembler
Der GNU Assembler, häufig als GAS bezeichnet, ist ein Teil der GNU Binutils und ein wesentlicher Bestandteil der GNU Compiler Collection (GCC). Hier sind einige wichtige Punkte über den GNU Assembler:
- Zweck und Funktion: GAS ist ein Assembler, der Quellcode in Assemblersprache in Maschinencode übersetzt. Dies ist ein wichtiger Schritt im Kompilierungsprozess, insbesondere bei der Umwandlung von Hochsprachen wie C oder C++ in ausführbare Programme.
- Unterstützte Architekturen: GAS unterstützt eine Vielzahl von Prozessorarchitekturen, darunter x86, ARM, MIPS, PowerPC und viele mehr. Diese Vielseitigkeit macht es zu einem äußerst nützlichen Werkzeug für die Entwicklung auf verschiedenen Plattformen.
- Syntax: GAS verwendet eine spezielle Syntax, die als AT&T-Syntax bekannt ist. Diese unterscheidet sich von der Intel-Syntax, die in vielen anderen Assemblern verwendet wird. Zum Beispiel verwendet GAS das Format opcode source, destination, während die Intel-Syntax opcode destination, source verwendet.
- Integration mit GCC: Da GAS ein Teil der GNU Binutils ist, arbeitet es nahtlos mit GCC zusammen. Wenn Sie ein Programm mit GCC kompilieren, wird der Quellcode in der Regel zuerst in Assemblercode und dann in Maschinencode umgewandelt, wobei GAS diese Umwandlung übernimmt.
- Befehlszeilenoptionen: GAS bietet eine Vielzahl von Befehlszeilenoptionen, die es Entwicklern ermöglichen, die Assemblierung fein abzustimmen. Einige gängige Optionen sind -o zum Festlegen des Ausgabedateinamens und -g zum Hinzufügen von Debugging-Informationen.
- Lizenz: GAS wird unter der GNU General Public License (GPL) veröffentlicht. Dies bedeutet, dass es frei verfügbar ist und jeder den Quellcode einsehen, modifizieren und weiterverbreiten kann, solange die Bedingungen der GPL eingehalten werden.
Präprozessor-Direktiven
| Direktive | Bedeutung |
|---|---|
| .align Ausrichtung , Füllwert, MaxAusrichtung | Ausrichtung bis zu einer bestimmten Speichergrenze.
|
| .ascii "string"... | .ascii erwartet kein oder mehrere durch Kommas getrennte Strings. Es fügt jeden String zu aufeinanderfolgenden Adressen zusammen. Die Strings werden nicht mit NULL beendet |
| .asciz "string"... | Wie .ascii, jedoch wird jedem String eine NULL angehägt, wie es zum Beispiel unter C üblich. |
| .balign[wl] Ausrichtung, Füllwert, MaxAusrichtung | Ausrichtung bis zu einer bestimmten Speichergrenze.
|
| .byte Werte | Erwartet Werte in Bytegröße, die durch Kommas getrennt sein können |
| .data Unterabschnitt | Daten werden in den Abschnitt ".data" abgelegt |
| .double flonums | Erwartet Gleitkommazahlen, die durch Kommas getrennt sein können |
| .equ symbol, expression | Definiert ein Symbol mit einem Wert (siehe auch .set) |
| .float flonums | Erwartet Gleitkommazahlen, die durch Kommas getrennt sein können |
| .global symbol .globl symbol |
Macht ein Symbol global bekannt |
| .hword Werte | Erwartet Werte in 16-Bit-Zahl-Wert, die durch Kommas getrennt sein können |
| .include "file" | Inkludiert eine Datei, die Code oder Daten beinhaltet. Der Inhalt wird an der Stelle des .include-Befehls eingefügt |
| .int Werte | Erwartet Werte, die durch Kommas getrennt sein können. Die Größe ist abhängig von der Kompilierungsumgebung |
| .long Werte | Wie .int |
| .octa Werte | Erwartet Werte in 16 bytes-Zahl-Wert, die durch Kommas getrennt sein können |
| .org new-pos , fill | Richtet den Code an die "new-pos" an. Mit "fill" (optional) wird angegeben, mit welchen Werten der übersprungen Speicher gefüllt wird |
| .quad Werte | Erwartet Werte in 8 bytes-Zahl-Wert, die durch Kommas getrennt sein können |
| name .req registername | Dadurch wird ein Alias für den Registernamen mit dem Namen "name" erstellt . Beispiel: Register0 .req x0 |
| .set symbol, expression | Definiert ein Symbol mit einem Wert |
| .short Werte | Erwartet Werte, die durch Kommas getrennt sein können. Die Größe ist abhängig von der Kompilierungsumgebung |
| .single flonums | Wie .float |
| .space größe, fill | belegt Speicher von "größe" gefüllt optional mit "fill" |
| .string "str" | Wie .asciz |
| .unreq alias-name | Dadurch wird ein Registeralias gelöscht, der zuvor mit der .req Direktive definiert wurde. |
Bedingte Assemblierung
.if .else .endif
Makros
.macro
Mit den Befehlen .macro und .endm können Sie Makros definieren, die Assembly-Ausgaben generieren. Diese Definition gibt beispielsweise ein Makro an, sum das eine Zahlenfolge in den Speicher einfügt:
.macro sum from=0, to=5
.long \from
.if \to-\from
sum "(\from+1)",\to
.endif
.endm
Mit dieser Definition, SUM 0,5 ist gleichwertig mit dieser Assemblyeingabe:
.long 0
.long 1
.long 2
.long 3
.long 4
.long 5
.macro macname .macro macname macargs …
- Begin the definition of a macro called macname. If your macro definition requires arguments, specify their names after the macro name, separated by commas or spaces. You can qualify the macro argument to indicate whether all invocations must specify a non-blank value (through ‘:req’), or whether it takes all of the remaining arguments (through ‘:vararg’). You can supply a default value for any macro argument by following the name with ‘=deflt’. You cannot define two macros with the same macname unless it has been subject to the .purgem directive (see .purgem name) between the two definitions. For example, these are all valid .macro statements:
- Damit beginnt die Definition eines Makros namens macname . Wenn die Makrodefinition Argumente erfordert, werden nach dem Macronamen ihre Namen übergeben, die per Komma oder Leerzeichen definiert werden können. Jedem Argument kann ein Standardwert zugeordnet werden, indem dem Namen ein "= def" zugeordnet wird.
.macro comm
- Der Beginn eines Makros ohne Argumente.
.macro plus1 p, p1 .macro plus1 p p1
- Erzeugt ein Makro namens plus1, das zwei Argumente annimmt. Beide Schreibweisen sind möglich. Um auf die Argumente innerhalb des Makros zuzugreifen wird \p oder \p1 verwendet.
.macro reserve_str p1=0 p2
- Erzeugt ein Makro namens reserve_str, das zwei Argumente annimmt. Das erste Argument "p1" hat als Standardwert "0", wenn dieses nicht angegeben wurde. Wird ein Parameter übergeben, wird der Standardwert überschrieben. Um "nur" das zweite Argument "p2" anzusprechen wird das Makro wie volgt aufgerufen:
reserve_str ,b
.macro m p1:req, p2=0, p3:vararg
- Erzeugt ein Makro namens m, das mind. drei Argumente annimmt. Das erste Argument muss mit übergeben werden. Hier wird das Schlüsselwort :req verwendet, welches besagt, dass ein Wert mit übergeben werden muss. Das zweite Argument kann übergeben werden, wenn dieser kein Argument enthält, wird der Standradwert "0" verwendet. Das dritte Argument bekommt alle Werte zugesprochen, die sonst noch angegeben wurden. Hier wird Schlüsselwort :vararg verwendet.
.endm
- Definiert das Ende des Makros.
.exitm
- Definiert ein vorzeitiges Ende des Makros.
\@ (Backslash-At):
- \@ ist eine Erweiterung, die eine eindeutige, numerische Kennung für jedes Auftreten eines Makros generiert. Diese Nummer ändert sich bei jedem Aufruf des Makros und kann daher verwendet werden, um eindeutige Namen für Labels, Register oder Variablen zu erstellen.
- Die generierte Zahl ist immer gleich innerhalb desselben Makroaufrufs, aber sie ändert sich bei jedem neuen Makroaufruf.
- Beispiel:
.macro UNIQUE_LABEL label_\@: .endm
- Bei jedem Aufruf des Makros UNIQUE_LABEL wird label_\@ zu einem eindeutigen Label wie label_1, label_2 usw.
\+ (Backslash-Plus):
- \+ ist ein Zähler, der jedes Mal erhöht wird, wenn er verwendet wird. Anders als \@ bleibt \+ nicht auf den Bereich eines Makros beschränkt und kann über mehrere Makroaufrufe hinweg zunehmen.
- Es kann nützlich sein, um eindeutig nummerierte Labels oder Variablen zu erstellen, die eine sequentielle Reihenfolge benötigen.
- Beispiel:
.macro INCREMENT_LABEL label_\+: .endm
- Bei jedem Aufruf des Makros INCREMENT_LABEL erhöht sich der Wert von \+ um eins, sodass die Labels aufeinanderfolgende Nummern wie label_0, label_1, label_2 usw. erhalten.
Präfixoperator
| - | negiert den absolute Wert |
| ~ | Bitweise NOT vom absoluten Wert |
Infix-Operatoren
Nach Priorität sortiert:
| * | Multiplikation |
| / | Division |
| % | Rest |
| << | Bitweise nach links verschieben |
| >> | Bitweise nach rechts verschieben |
| ǀ | Bitweises Inklusives ODER |
| & | Bitweises UND |
| ^ | Bitweises Exklusiv-Oder |
| ! | Bitweise oder nicht |
| + | Addition |
| - | Subtraktion |
| == | Ist gleich |
| <> != |
Ist nicht gleich |
| < | Ist kleiner als |
| > | Ist größer als |
| >= | Ist größer als oder gleich |
| <= | Ist kleiner als oder gleich |
| && | Logisches UND |
| ǀǀ | Logisches ODER |
C to Assembly
GCC can be incredibly useful when first starting to learn any assembly language because it provides an option to generate assembly output from source code using the -S option. If you want to generate assembly with source code, compile with -g and -c options, then dump with objdump -d -S. Most people want their applications optimized for speed rather than size, so it stands to reason the GNU C optimizer is not terribly efficient at generating compact code. Our new A.I overlords might be able to change all that, but at least for now, a human wins at writing compact assembly code.
Just to illustrate using an example. Here’s a subroutine that does nothing useful.
- include <stdio.h>
void calc(int a, int b) {
int i;
for(i=0;i<4;i++) {
printf("%i\n", ((a * i) + b) % 5);
}
} Compile this code using -Os option to optimize for size. The following assembly is generated by GCC. Recall that x30 is the link register and saved here because of the call to printf. We also have to use callee saved registers x19-x22 for storing variables because x0-x18 are trashed by the call to printf.
.arch armv8-a .file "calc.c" .text .align 2 .global calc .type calc, %function calc: stp x29, x30, [sp, -64]! // store x29, x30 (LR) on stack add x29, sp, 0 // x29 = sp stp x21, x22, [sp, 32] // store x21, x22 on stack adrp x21, .LC0 // x21 = "%i\n" stp x19, x20, [sp, 16] // store x19, x20 on stack mov w22, w0 // w22 = a mov w19, w1 // w19 = b add x21, x21, :lo12:.LC0 // x21 = x21 + 0 str x23, [sp, 48] // store x23 on stack mov w20, 4 // i = 4 mov w23, 5 // divisor = 5 for modulus .L2: sdiv w1, w19, w23 // w1 = b / 5 mov x0, x21 // x0 = "%i\n" add w1, w1, w1, lsl 2 // w1 *= 5 sub w1, w19, w1 // w1 = b - ((b / 5) * 5) add w19, w19, w22 // b += a bl printf
subs w20, w20, #1 // i = i - 1 bne .L2 // while (i != 0)
ldp x19, x20, [sp, 16] // restore x19, x20 ldp x21, x22, [sp, 32] // restore x21, x22 ldr x23, [sp, 48] // restore x23 ldp x29, x30, [sp], 64 // restore x29, x30 (LR) ret // return to caller
.size calc, .-calc .section .rodata.str1.1,"aMS",@progbits,1 .LC0: .string "%i\n" .ident "GCC: (Debian 6.3.0-18) 6.3.0 20170516" .section .note.GNU-stack,"",@progbits i is initialized to 4 instead of 0 and decreased rather than increased. There’s no modulus instruction in the A64 set, and division instructions don’t produce a remainder, so the calculation is performed using a combination of division, multiplication and subtraction. The modulo operation is calculated with the following : R = N - ((N / D) * D)
N denotes the numerator/dividend, D denotes the divisor and R denotes the remainder. The following assembly code is how it might be written by hand. The most notable change is using the msub instruction in place of a separate add and sub.
.arch armv8-a
.text .align 2 .global calc
calc:
stp x19, x20, [sp, -48]!
stp x21, x22, [sp, 16]
stp x23, x30, [sp, 32]
mov w19, w0 // w19 = a mov w20, w1 // w20 = b
mov w21, 4 // i = 4
mov w22, 5 // set divisor .LC2: sdiv w1, w20, w22 // w1 = b - ((b / 5) * 5) msub w1, w1, w22, w20 // adr x0, .LC0 // x0 = "%i\n" bl printf
add w20, w20, w19 // b += a
subs w21, w21, 1 // i = i - 1 bne .LC2 //
ldp x19, x20, [sp], 16
ldp x21, x22, [sp], 16
ldp x23, x30, [sp], 16
ret .LC0: .string "%i\n" Use compiler generated assembly as a guide, but try to improve upon the code as shown in the above example.
Symbolic Constants
What if we want to use symbolic constants from C header files in our assembler code? There are two options.
Convert each symbolic constant to its GAS equivalent using the .EQU or .SET directives. Very time consuming. Use C-style #include directive and pre-process using GNU CPP. Quicker with several advantages. Obviously the second option is less painful and less likely to produce errors. Of course, I’m not discounting the possibility of automating the first option, but why bother? CPP has an option that will do it for us. Let’s see what the manual says.
Instead of the normal output, -dM will generate a list of #define directives for all the macros defined during the execution of the preprocessor, including predefined macros. This gives you a way of finding out what is predefined in your version of the preprocessor.
-dM will dump all the #define macros and -E will preprocess a file, but not compile, assemble or link. The steps required before using symbolic names in our assembler code are as follows:
Use cpp -dM to dump all the #defined keywords from each include header. Use sort and uniq -u to remove duplicates. Use the #include directive in our assembly source code. Use cpp -E to preprocess and pipe the output to a new assembly file. (-o is an output option) Assemble using as to generate an object file. Link the object file to generate an executable.
The following is some simple code that displays Hello, World! to the console.
- include "include.h"
.global _start
.text
_start:
mov x8, __NR_write
mov x2, hello_len
adr x1, hello_txt
mov x0, STDOUT_FILENO
svc 0
mov x8, __NR_exit
svc 0
.data
hello_txt: .ascii "Hello, World!\n" hello_len = . - hello_txt Preprocess the above source using CPP -E. The result of this will be replacing each symbolic constant used with its assigned numeric value.
Finally, assemble using GAS and link with LD.
The following two directives are examples of simple text substitution or symbolic constants.
#define FALSE 0 #define TRUE 1
The equivalent can be accomplished with the .EQU or .SET directives in GAS.
.equ TRUE, 1 .set TRUE, 1 .equ FALSE, 0 .set FALSE, 0
Personally, I think it makes more sense to use the C preprocessor, but it’s entirely up to yourself.
Structures and Unions
A structure in programming is useful for combining different data types into a single user-defined data type. One of the major pitfalls in programming any assembly is poorly managed memory access. In my own experience, MASM always had the best support for data structures while NASM and YASM could be much better. Unfortunately support for structures in GAS isn’t great. Understandably, many of the hand-written assembly programs for Linux normally use global variables that are placed in the .data section of a source file. For a Position Independent Code (PIC) or thread-safe application that can only use local variables allocated on the stack, a data structure helps as a reference to manage those variables. Assigning names helps clarify what each stack address is for, and improves overall quality. It’s also much easier to modify code by simply re-arranging the elements of a structure later.
Take for example the following C structure dimension_t that requires conversion to GAS assembly syntax.
typedef struct _dimension_t {
int x, y;
} dimension_t; The closest directive to the struct keyword is .struct. Unfortunately this directive doesn’t accept a name and nor does it allow members to be enclosed between .struct and .ends that some of you might be familiar with in YASM/NASM. This directive only accepts an offset as a start position.
.struct 0
dimension_t.x:
.struct dimension_t.x + 4
dimension_t.y:
.struct dimension_t.y + 4
dimension_t_size: An alternate way of defining the above structure can be done with the .skip or .space directives.
.struct 0
dimension_t.x: .skip 4 dimension_t.y: .skip 4 dimension_t_size: If we have to manually define the size of each field in the structure, it seems the .struct directive is of little use. Consider using the #define keyword and preprocessing the file before assembling.
- define dimension_t.x 0
- define dimension_t.y 4
- define dimension_t.size 8
For a union, it doesn’t get any better than what I suggest be used for structures. We can use the .set or .equ directives or refer back to a combination of using #define and cpp. Support for both unions and structures in GAS leaves a lot to be desired.
Operators
From time to time I’ll see some mention of “polymorphic” shellcodes where the author attempts to hide or obfuscate strings using simple arithmetic or bitwise operations. Usually the obfuscation is done via a bit rotation or exclusive-OR and this presumably helps evade detection by some security products.
Operators are arithmetic functions, like + or %. Prefix operators take one argument. Infix operators take two arguments, one on either side. Operators have precedence, but operations with equal precedence are performed left to right.
Precedence Operators Highest Mutiplication (*), Division (/), Remainder (%), Shift Left (<<), Right Shift (>>). Intermediate Bitwise inclusive-OR (|), Bitwise And (&), Bitwise Exclusive-OR (^), Bitwise Or Not (!). Low Addition (+), Subtraction (-), Equal To (==), Not Equal To (!=), Less Than (<), Greater Than (>), Greater Than Or Equal To (>=), Less than Or Equal To (<=). Lowest Logical And (&&). Logical Or (||). The following examples show a number of ways to use operators prior to assembly. These examples just load the immediate value 0x12345678 into the w0 register.
// exclusive-OR movz w0, 0x5678 ^ 0x4823 movk w0, 0x1234 ^ 0x5412 movz w1, 0x4823 movk w1, 0x5412, lsl 16 eor w0, w0, w1
// rotate a value left by 5 bits using MOVZ/MOVK movz w0, (0x12345678 << 5) | (0x12345678 >> (32-5)) & 0xFFFF movk w0, ((0x12345678 << 5) >> 16) | ((0x12345678 >> (32-5)) >> 16) & 0xFFFF, lsl 16 // then rotate right by 5 to obtain original value ror w0, w0, 5
// right rotate using LDR .equ ROT, 5
ldr w0, =(0x12345678 << ROT) | (0x12345678 >> (32 - ROT)) & 0xFFFFFFFF ror w0, w0, ROT
// bitwise NOT ldr w0, =~0x12345678 mvn w0, w0
// negation ldr w0, =-0x12345678 neg w0, w0
Macros
If we need to repeat a number of assembly instructions, but with different parameters, using macros can be helpful. For example, you might want to eliminate branches in a loop to make code faster. Let’s say you want to load a 32-bit immediate value into a register. ARM instruction encodings are all 32-bits, so it isn’t possible to load anything more than a 16-bit immediate. Some immediate values can be stored in the literal pool and loaded using LDR, but if we use just MOV instructions, here’s how to load the 32-bit number 0x12345678 into register w0.
movz w0, 0x5678 movk w0, 0x1234, lsl 16
The first instruction MOVZ loads 0x5678 into w0, zero extending to 32-bits. MOVK loads 0x1234 into the upper 16-bits using a shift, while preserving the lower 16-bits. Some assemblers provide a pseudo-instruction called MOVL that expands into the two instructions above. However, the GNU Assembler doesn’t recognize it, so here are two macros for GAS that can load a 32-bit or 64-bit immediate value into a general purpose register.
// load a 64-bit immediate using MOV
.macro movq Xn, imm
movz \Xn, \imm & 0xFFFF
movk \Xn, (\imm >> 16) & 0xFFFF, lsl 16
movk \Xn, (\imm >> 32) & 0xFFFF, lsl 32
movk \Xn, (\imm >> 48) & 0xFFFF, lsl 48
.endm
// load a 32-bit immediate using MOV
.macro movl Wn, imm
movz \Wn, \imm & 0xFFFF
movk \Wn, (\imm >> 16) & 0xFFFF, lsl 16
.endm
Then if we need to load a 32-bit immediate value, we do the following.
movl w0, 0x12345678
Here are two more that imitate the PUSH and POP instructions. Of course, this only supports a single register, so you might want to write your own.
// imitate a push operation
.macro push Rn:req
str \Rn, [sp, -16]
.endm
// imitate a pop operation
.macro pop Rn:req
ldr \Rn, [sp], 16
.endm
Conditional assembly
Like the GNU C compiler, GAS provides support for if-else preprocessor directives. The following shows an example in C.
#ifdef BIND
// compile code to bind
#else
// compile code to connect
#endif
Next, an example for GAS.
.ifdef BIND
// assemble code to bind
.else
// assemble code for connect
.endif
GAS also supports something similar to the #ifndef directive in C.
.ifnotdef BIND
// assemble code for connect
.else
// assemble code for bind
.endif
Comments
These are ignored by the assembler. Intended to provide an explanation for what code does. C style comments /* */ or C++ style // are a good choice. Ampersand (@) and hash (#) are also valid, however, you should know that when using the preprocessor on an assembly source code, comments that start with the hash symbol can be problematic. I tend to use C++ style for single line comments and C style for comment blocks.
# This is a comment
// This is a comment
/* This is a comment */
@ This is a comment.