Zygentoma    Zygentoma

Weiterführende Links:

keine

Eigene malloc-Funktion

Einleitung

Ich will zeigen, wie man unter Linux eine einfache malloc-Funktion implementieren kann. Mein malloc ist nicht besonders effizient, es läuft in O(n), das heißt, wenn man doppelt so viel Speicher reserviert hat (in gleichgroßen Blöcken), dauert auch alloziieren von neuem Speicher doppelt so lange. Man könnte das etwas effizienter gestalten aber das soll hier egal sein. Das original malloc wird auf der Seite von Doug Lea beschrieben. Meine malloc Funktion ist in Assembler und C geschrieben und verwendet nach außen hin nur den Linux Syscall SYS_BRK. Der Asm-Code ist 64bit Code. Eine übertragung auf 32bit ist möglich, aber dabei muss beachtet werden, dass die CallingConventions dabei andere sind! Dazu auch: Syscalls

Für Kommentare oder Fragen, nutzt bitte das Gästebuch.

Vorraussetzung: Assembler für die Syscalls

%include "syscall_64_syscall.inc"

extern cmain

global _start
global sys_exit
global sys_brk


BITS 64

section .text

_start:
    call cmain

    ;; CLEAN UP ---
    mov rdi,0
    call sys_exit
    ;; --- CLEAN UP


sys_exit:
    mov rax,_sys_exit
    syscall
    ret

sys_brk:
    mov rax,_sys_brk
    syscall
    ret

section .data

section .bss
resb 1
Wichtig ist hier das "section .bss", das hier mit einem uninitialisiertem Byte gefüllt wird, weil der Heap direkt nach .bss angelegt wird.

Unterbrechung: Etwas Theorie

Es gibt drei Orte, an denen wir etwas über die Größe und den Status eines Blockes herausfinden wollen:

  1. Am Anfang des Blockes, um herauszufinden, ob der Block frei ist, und wo der nächste Block liegt
  2. Am Anfang des Datenbereiches, um herauszufinden, wie viel freigegeben werden soll, wenn free() aufgerufen wird
  3. Am Ende des Blockes, um herauszufinen, ob der vorherige Block auch frei ist, und die Große, falls er das ist, zum vereinigen zu einem größeren freien Block
Wir brauchen dabei zunächst ein Bit, das kennzeichnet, ob der Block belegt (0) ist, oder frei (1). Da wir in Bytes arbeiten, haben wir noch 7 Bit zur Verfügung, um eine Zahl < 128 zu Speichern. Darin Speichern wir die Größe des Blockes, sofern der Datenbereich < 126 Bytes lang ist.
[flgsze][data < 126 ... ][flgsze]

Sollten wir allerdings mal mehr als 125 Bytes Daten benötigen, speichern wir im oberene Teil des Flagbytes nur 0 und packen dafür noch zwei 64bit Integer vor und hinter den Datenbereich. Damit man das auch herausfindet, wenn man nur den Pointer auf den Datenbereich hat (free()), setzen wir das Flagbyte nochmal direkt vor den Datenbereich:

[flgnul][____________________________ size ____________________________][flgnul]
[data > 125 ... ][____________________________ size ____________________________][flgnul]
Das hat zur Folge, dass es sehr viel ineffizienter ist, 126 Byte zu alloziieren, als 125 Byte zu alloziieren. Allerdings beträgt der Overhead bei 126 Bytes nur gut 15% und nimmt bei größeren Datenmenge stark ab (1,9% bei 1 KiB, 0,0018% bei 1 MiB). Im Gegensatz dazu beträgt der Overhead bei der Reservierung einzelner Bytes 200%!

Fortsetzung: C für die Funktionen

Zu SYS_BRK ist noch zu sagen, dass es nicht direkt vergrößert oder verkleinert, sondern nur das Ende des Heaps setzt. Das aktuelle Ende des Heaps kann man mit SYS_BRK(0) herausfinden. Den Anfang des Heaps muss man sich merken!

#define int long long // unschoen, aber erfuellt seinen zweck

extern int sys_exit(int s);
extern void* sys_brk(void* s);

// Schreibe Structurdaten
// Flag in Abhaengigkeit von free und
// Datengroesse an geeignete Stelle
void insertstructure(void* ptr, unsigned int size, unsigned char free)
{
    if (free)
    free = 1;
    if (size < 128)
    {
        *((unsigned char*)ptr) = (size << 1) | free;
        *((unsigned char*)ptr+size-1) = (size << 1) | free;
    }
    else
    {
        *((char*)ptr) = free;
        *((char*)ptr+9) = free;
        *((unsigned int*)(ptr+1)) = size;
        *((char*)ptr+size-1) = free;
        *((unsigned int*)(ptr+size-9)) = size;
    }
}

// Alloziiere an Spitze vom Heap, ohne nach freien Bloecken zu suchen
void* qmalloc(unsigned int s)
{
    unsigned int size = s + (s < 126 ? 2 : 19);
    void *ptr = sys_brk(0);
    sys_brk(ptr+size);
    insertstructure(ptr, size,0);
    return ((size < 128) ? ptr+1 : ptr+10);
}

// Suche nach freien Bloecken, die gross genug sind
// ansonsten wird qmalloc aufgerufen
void* malloc(unsigned int s)
{
    static void* startptr = 0;
    void* ptr = sys_brk(0);
    if (!startptr)
    {
        startptr = ptr+9;
        sys_brk(startptr);
        *((int*)ptr) = 0;
        *((char*)ptr+8) = 0;
    }
   
    unsigned int size = s + (s < 126 ? 2 : 19);
   
    void *moveptr;
    unsigned int actualsize;
    for (moveptr = startptr; moveptr < ptr; moveptr += actualsize)
    {
        actualsize = (*((unsigned char*)moveptr)) >> 1;
        if (!actualsize)
            actualsize = *((unsigned int*)(moveptr+1));
       
        if (((*((char*)moveptr)) & 0x01) && (actualsize >= size))
        {
            insertstructure(moveptr, size,0);
           
           
            if (actualsize != size)
            {
                unsigned int othersize = actualsize - size;
                insertstructure(moveptr+size, othersize, 1);
            }
           
            return ((size < 128) ? moveptr+1 : moveptr+10);
        }
    }
    return qmalloc(s);
}

// Vereinige mit anliegenden freien Bloecken
void free(void* ptr)
{
    unsigned int size = (*((unsigned char*)(--ptr))) >> 1;
    if (!size)
    {
        size = *((unsigned int*)(ptr-8));
        ptr-=9;
    }
    //if (ptr + size > sys_brk(0))
        // ggf Fehlermeldung ausgeben
    if (ptr + size < sys_brk(0))
        if ((*((unsigned char*)(ptr+size))) & 1)
        {
            unsigned int oversize = (*((unsigned char*)(ptr+size))) >> 1;
            if (!oversize)
            oversize = *((unsigned int*)(ptr+size+1));
            size += oversize;
        }
   
    if ((*((unsigned char*)(ptr-1))) & 1)
    {
        unsigned int undersize = (*((unsigned char*)(ptr-1))) >> 1;
        if (!undersize)
        undersize = *((unsigned int*)(ptr-9));
        ptr -= undersize;
        size += undersize;
    }
    insertstructure(ptr, size, 1);
}

Ende: Zum Schluss

Noch etwas zur Laufzeitbetrachtung: Wer häufig kleine Speichermengen reserviert und nur selten freigibt, sollte versuchen, qmalloc zu benutzen, das in konstanter Zeit läuft. Mit -O3 kompiliert, ist qmalloc(60) nur noch 9 inline Maschinenbefehle lang:

<test+1>:      xor    edi,edi
<test+3>:      call   0x40012c <sys_brk>
<test+8>:      lea    rdi,[rax+0x34]
<test+12>:     mov    rbx,rax
<test+15>:     call   0x40012c <sys_brk>
<test+20>:     mov    BYTE PTR [rbx],0x68
<test+23>:     mov    BYTE PTR [rbx+0x33],0x68
<test+27>:     add    rbx,0x1
<test+31>:     mov    rax,rbx


Valid XHTML 1.0 Transitional Valid CSS!