Assemblersprache - Speicherverwaltung
Die Speicherverwaltung ist ein wichtiger Teil der Systemprogrammierung. In Assemblersprache können Sie Speicher dynamisch über Systemaufrufe zuweisen und freigeben, Speicheradressen direkt manipulieren und eine effiziente Speicherverwaltung erreichen.
Speicherlayout eines Programms
Ein laufendes Programm ist im Speicher wie folgt verteilt:
Das Speicherlayout eines Linux-Prozesses sieht wie folgt aus:
高地址 (0xFFFFFFFF) +----------------------+ | 内核空间 | 用户程序不可访问 +----------------------+ (0xC0000000) | 栈 (Stack) | <-- ESP 指向栈顶 | 向下增长 | +----------------------+ | | | 内存映射区域 | mmap 分配的区域 | | +----------------------+ | 堆 (Heap) | <-- brk/sbrk 管理 | 向上增长 | +----------------------+ | BSS 段 (.bss) | 未初始化全局变量 +----------------------+ | 数据段 (.data) | 已初始化全局变量 +----------------------+ | 代码段 (.text) | 程序指令(只读) +----------------------+ 低地址 (0x08048000)
Dynamische Speicherzuweisung: brk-Systemaufruf
sys_brk(Systemaufrufnummer 45) durch Anpassen derDatensegmentgrenze des Programms (Program Break)zum Zuweisen/Freigeben von Speicher.
Der Program Break ist die Endposition des Datensegments (einschließlich .data, .bss und des Heaps). Das Programm kann nicht auf Speicher darüber zugreifen.
| Aufrufmethode | Beschreibung | Rückgabewert |
|---|---|---|
ebx = 0 | Aktuelle Break-Adresse abrufen | EAX = aktuelle Break-Adresse |
ebx = 新地址 | Neue Break-Adresse festlegen | EAX = neue Break-Adresse (gibt bei Fehlschlag die ursprüngliche Adresse zurück) |
Beispiel
; Verwenden Sie sys_brk, um Speicher dynamisch zuzuweisen
section .data
alloc_msg db 'Memory allocated successfully at: 0x'
alloc_len equ $ - alloc_msg
newline db 0xA
section .bss
orig_break resd 1 ; Speichern der ursprünglichen Break-Adresse
section .text
global _start
_start:
; 1. Aktuellen Program Break abrufen
mov eax, 45 ; sys_brk
mov ebx, 0 ; ebx=0 bedeutet aktuellen Break abfragen
int 0x80
mov [orig_break], eax ; Ursprüngliche Break-Adresse speichern
; 2. 4096 Bytes (4 KB) Speicher zuweisen
mov ebx, eax ; Aktuelle Break-Adresse
add ebx, 4096 ; Um 4096 Bytes erhöhen
mov eax, 45 ; sys_brk
int 0x80
; EAX = neue Break-Adresse (d. h. das Ende des zugewiesenen Bereichs)
; 3. Der neu zugewiesene Speicher liegt zwischen orig_break und eax-1
; Sie können diesen Speicherbereich sicher lesen und schreiben
mov ebx, [orig_break] ; Startadresse des zugewiesenen Bereichs abrufen
mov dword [ebx], 42 ; 42 in den neuen Speicher schreiben
mov eax, [ebx] ; Zurücklesen
; 4. Speicher freigeben: Break auf die ursprüngliche Position zurücksetzen
mov eax, 45 ; sys_brk
mov ebx, [orig_break] ; Auf den ursprünglichen Break zurücksetzen
int 0x80
mov eax, 1
mov ebx, 0
int 0x80
sys_brk kann nur die Grenze des kontinuierlichen Datensegments anpassen und keinen Speicher in der Mitte freigeben. Um einen zugewiesenen Speicherblock freizugeben, müssen Sie zuerst allen danach zugewiesenen Speicher freigeben. Aus diesem Grund verwenden moderne Programme häufiger mmap oder die malloc-Bibliotheksfunktionen.
Verwendung von mmap zum Zuweisen von Speicher (Empfohlen)
sys_mmap2(Systemaufrufnummer 192) ist flexibler; er kann mehrere Speicherblöcke unabhängig voneinander zuweisen und freigeben:
Beispiel
; mmap verwenden, um unabhängig freigebbaren Speicher zuzuweisen
section .data
; mmap-bezogene Konstanten
PROT_READ equ 1
PROT_WRITE equ 2
MAP_PRIVATE equ 2
MAP_ANONYMOUS equ 0x20
section .bss
mem_block resd 1 ; Zugewiesene Speicheradresse speichern
section .text
global _start
_start:
; mmap-Aufruf: 4096 Bytes anonymen Speicher zuweisen
mov eax, 192 ; sys_mmap2
mov ebx, 0 ; Kernel die Adresse auswählen lassen
mov ecx, 4096 ; Zuweisungsgröße: 4 KB
mov edx, PROT_READ | PROT_WRITE ; Lesbar und schreibbar
mov esi, MAP_PRIVATE | MAP_ANONYMOUS ; Private anonyme Zuordnung
mov edi, -1 ; Dateideskriptor (-1 für anonyme Zuordnung verwenden)
mov ebp, 0 ; Versatz (0 für anonyme Zuordnung verwenden)
int 0x80
; EAX = zugewiesene Speicheradresse (gibt bei Fehlschlag einen negativen Wert zurück)
cmp eax, -4096 ; Prüfen, ob es fehlgeschlagen ist
ja mmap_failed ; Wenn es zwischen -4095 und -1 liegt, ist es fehlgeschlagen
mov [mem_block], eax ; Speicheradresse speichern
; Zugewiesenen Speicher verwenden: Daten schreiben
mov ebx, [mem_block]
mov dword [ebx], 0x12345678 ; 4 Bytes schreiben
mov dword [ebx + 4], 'runo' ; "runo" schreiben
mov dword [ebx + 8], 'ob!!' ; "ob!!" schreiben
; Speicher freigeben: munmap
mov eax, 91 ; sys_munmap
mov ebx, [mem_block] ; Speicheradresse
mov ecx, 4096 ; Freigabegröße
int 0x80
jmp exit
mmap_failed:
; Fehler behandeln...
exit:
mov eax, 1
mov ebx, 0
int 0x80
Lese- und Schreiboperationen im Speicher
In Assemblersprache erfolgen alle Speicherzugriffe übermovBefehle in Kombination mit eckigen Klammern:
Beispiel
section .data
var1 db 0x55 ; 1 Byte
var2 dw 0x1234 ; 2 Bytes
var3 dd 0x12345678 ; 4 Bytes
section .text
global _start
_start:
; Speicher verschiedener Größen lesen
mov al, [var1] ; 1 Byte lesen: al = 0x55
mov ax, [var2] ; 2 Bytes lesen: ax = 0x1234
mov eax, [var3] ; 4 Bytes lesen: eax = 0x12345678
; Schreiben von Speicher unterschiedlicher Größe
mov byte [var1], 0xAA ; 1 Byte schreiben
mov word [var2], 0xABCD ; 2 Bytes schreiben
mov dword [var3], 0xDEADBEEF ; 4 Bytes schreiben
; Zugriff auf Speicher über Zeiger
mov ebx, var3 ; ebx zeigt auf var3
mov eax, [ebx] ; Wert von var3 indirekt lesen
add dword [ebx], 1 ; var3 = var3 + 1
mov eax, 1
mov ebx, 0
int 0x80
Sicherheitsrichtlinien für Speicheroperationen
Bei Speicheroperationen in Assembler gibt es kein "Sicherheitsnetz"; beachten Sie die folgenden Regeln:
| Regel | Beschreibung | Konsequenz des Verstoßes |
|---|---|---|
| Kein Zugriff außerhalb der Grenzen | Nicht mehr als die Größe der Variable/des Puffers lesen/schreiben | Datenbeschädigung, Speicherzugriffsfehler (Segmentation Fault) |
| Keinen uninitialisierten Speicher lesen | Werte im .bss-Abschnitt sind undefiniert | Unvorhersehbare Ergebnisse |
| Nicht an ungültigen Adressen lesen/schreiben | Sicherstellen, dass der Zeiger auf gültigen Speicher zeigt | Speicherzugriffsfehler (Segmentation Fault) |
| Nicht in schreibgeschützten Speicher schreiben | Der .text-Abschnitt und der Konstantenbereich sind nicht beschreibbar | Speicherzugriffsfehler (Segmentation Fault) |
| Adresstypausrichtung | Wortzugriffe verwenden gerade Adressen; Doppelwortzugriffe verwenden Vielfache von 4 | Leistungseinbußen oder Busfehler |
Beispiel
section .data
small_buf db 0, 0, 0, 0 ; Nur 4 Bytes
section .text
global _start
_start:
; Gefährliche Operation 1: Schreiben außerhalb der Grenzen
; mov dword [small_buf + 3], 0x12345678
; Dies überschreibt 3 Bytes an Daten nach small_buf!
; Gefährliche Operation 2: Schreiben in den Codeabschnitt (schreibgeschützt)
; mov dword [_start], 0x90 ; Der Versuch, Code zu modifizieren, verursacht einen Speicherzugriffsfehler
; Gefährliche Operation 3: Nullzeiger / ungültige Adresse
; mov eax,
; mov
; Sicherer Ansatz: immer innerhalb des zugewiesenen Speicherbereichs arbeiten
mov dword [small_buf], 'runo' ; Korrekt: innerhalb von 4 Bytes arbeiten
mov eax, 1
mov ebx, 0
int 0x80
Stapel-Speicherverwaltung (Stack)
Der Stapel (Stack) ist ein weiterer wichtiger Speicherbereich, der über die PUSH/POP-Anweisungen und das ESP-Register verwaltet wird:
Beispiel
section .data
original_esp dd 0
section .text
global _start
_start:
mov [original_esp], esp ; Den ursprünglichen Stapelzeiger speichern
; PUSH: auf den Stapel legen (ESP -= 4, Daten schreiben)
push dword 100 ; 100 auf den Stapel legen
; ESP = ESP - 4, [ESP] = 100
push dword 200 ; 200 auf den Stapel legen
push dword 300 ; 300 auf den Stapel legen
; Stapel-Layout:
; ESP+0: 300
; ESP+4: 200
; ESP+8: 100
; POP: vom Stapel nehmen (Daten lesen, ESP += 4)
pop eax ; eax = 300, ESP += 4
pop ebx ; ebx = 200, ESP += 4
pop ecx ; ecx = 100, ESP += 4
; Lokalen Speicher auf dem Stapel zuweisen
sub esp, 256 ; 256 Bytes auf dem Stapel zuweisen
; Nun können die 256 Bytes von ESP bis ESP+255 sicher verwendet werden
mov dword [esp], 42 ; 42 in den lokalen Speicher schreiben
add esp, 256 ; Lokalen Speicher freigeben
; Sicherstellen, dass ESP zu seiner ursprünglichen Position zurückkehrt!
mov eax, 1
mov ebx, 0
int 0x80
Die wichtigste Regel für Stapeloperationen:PUSH und POP müssen paarweise verwendet werden. Bevor eine Funktion zurückkehrt, müssen Sie sicherstellen, dass ESP zur korrekten Position zurückkehrt; andernfalls holt RET die falsche Rückspraddresse vom Stapel, was zum Absturz des Programms oder zur Ausführung von beliebigem Code führt. Dies ist einer der fatalsten Bugs in der Assembler-Programmierung.
Vergleich von Speicherverwaltungsmethoden
| Schema | Flexibilität | Komplexität | Anwendungsszenario |
|---|---|---|---|
| Globale Variablen (.data/.bss) | Gering (zur Kompilierzeit festgelegt) | Am niedrigsten | Daten mit fester Größe |
| Stapel-Zuweisung (sub esp) | Mittel (dynamisch innerhalb der Funktion) | Niedrig | Lokale Variablen von Funktionen |
| brk/sbrk | Mittel (kann nur wachsen/schrumpfen) | Mittel | Zusammenhängender Speicherbereich |
| mmap | Hoch (beliebige Zuweisung/Freigabe) | Hoch | Wenn eine flexible Speicherverwaltung erforderlich ist |