顯示具有 C++ 標籤的文章。 顯示所有文章
顯示具有 C++ 標籤的文章。 顯示所有文章

2013年7月31日 星期三

dlopen() 與 GCC -rdynamic 選項

之前讀某個 JIT Compiler 的程式碼。只要正確輸入函式原型,這個 JIT Compiler 可以讓使用者呼叫外部的 C 函式。

要實作類似的功能並不困難,我們只要寫一個「名稱—位址」的對照表,JIT Compiler 就能輕易地實作這個功能。舉例來說,如果我希望使用者可以使用 foo 與 bar 二個函式,我們需要定義一個簡單的函式表:

struct { const char *, void * } func_table[] = {
  { "foo", (void *)&foo },
  { "bar", (void *)&bar },
  { NULL, NULL },
};


當使用者輸入 foo(),我們就可以從函式表取得 foo 的位址,進而呼叫 foo 函式。不過我卻沒有辦法在 JIT Compiler 的程式碼找到類似的東西。這激起了我的興趣。

其實 JIT Compiler 的執行檔確實是有這樣的函式表。如果仔細看該 JIT Compiler 的 Makefile,我們就可以發現他在編譯 JIT Compiler 的時候,額外加上 -rdynamic 選項。根據 GCC 使用手冊,-rdynamic 的用途為:
Pass the flag -export-dynamic to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of dlopen or to allow obtaining backtraces from within a program.
簡單的說,這個選項會把有的 non-static 函式視為要 export 的 symbol,讓 Linker 把他們都寫入 Dynamic Symbol Table。之後,我們就可以使用 dlsym() 找到這個 symbol。以下是一個簡單的範例:

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>


void foo() {
  printf("called foo()\n");
}

void bar() {
  printf("called bar()\n");
}

int main(int argc, char **argv) {
  if (argc < 2) {
    fprintf(stderr, "USAGE: %s [func]\n", argv[0]);
    return EXIT_FAILURE;
  }

  /* Load the function/variable symbol table from executable. */
  void *handle = dlopen(NULL, RTLD_LAZY);
  if (!handle) {
    fprintf(stderr, "ERROR: Failed to load executable\n");
    return EXIT_FAILURE;
  }

  /* Find the function */
  void *func = dlsym(handle, argv[1]);
  if (!func) {
    fprintf(stderr, "ERROR: Function not found: %s\n", argv[1]);
  } else {
    ((void (*)())func)();
  }

  dlclose(handle);

  return EXIT_SUCCESS;
}


我們可以分別使用以下二個指令編譯這個範例程式:

$ gcc example.c -ldl
$ gcc example.c -ldl -rdynamic

然後使用 objdump -T 指令觀看執行檔的 Dynamic Symbol Table。

$ objdump -T a.out

以下是簡化過的輸出:

a.out:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
... 略 ...
0000000000000000      DF *UND*    0000000000000000  GLIBC_2.2.5 fwrite
0000000000000000      DF *UND*    0000000000000000  GLIBC_2.2.5 dlsym
0000000000601048 g    D  *ABS*    0000000000000000  Base        _edata
0000000000400914 g    DF .text    0000000000000010  Base        bar
0000000000601038 g    D  .data    0000000000000000  Base        __data_start
0000000000400904 g    DF .text    0000000000000010  Base        foo
... 略 ...

0000000000400924 g    DF .text    00000000000000f6  Base        main
0000000000400788 g    DF .init    0000000000000000  Base        _init
0000000000601048 g    DO .bss    0000000000000008  GLIBC_2.2.5 stderr
... 略 ...


其中藍色的字就是加上 -rdynamic 選項多出來的資料。執行以下指令,我們就可以注意到範例程式可以呼叫對應的函式:

$ ./a.out foo
called foo()
$ ./a.out bar
called bar()

我們要怎麼取得 Dynamic Symbol Table 裡面的資料呢?這就必須借助 dlopen()dlsym()。根據 man dlopen 的資料,dlopen() 必須要傳入二個參數,第一個參數是要載入的動態函式庫,而第二個參數是函式庫的載入方式。如果我們以 NULL 作為第一個參數,則代表我們要載入執行檔本身的 Dynamic Symbol Table。接著我們就可以用 dlsym() 尋找我們要執行的函式了。

後注:雖然我覺得這個方法很巧妙也很有趣,不過我覺得這個方法不太安全。因為所有的函式都有可能暴露給 JIT Compiler 使用者。我覺得還是手動建函式表會比較好。這樣我們才可以控制要開放的函式與介面。

2011年8月8日 星期一

處理 Stack Overflow

直接看程式碼比較快:

#define _XOPEN_SOURCE 600

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <signal.h>

sigjmp_buf stack_overflow_guard;

char alter_stack[4096];

void sigsegv_handler(int sig) {
  if (sig == SIGSEGV) {
    /* Jump to catch block */
    siglongjmp(stack_overflow_guard, sig);
  }
}

void install_sigsegv_handler() {
  /* Prepare alternating stack */
  stack_t stk;

  stk.ss_sp = alter_stack;
  stk.ss_size = sizeof(alter_stack);
  stk.ss_flags = 0;

  if (sigaltstack(&stk, NULL) == -1) {
    fprintf(stderr, "ERROR: Unable to setup alternative stack.\n");
    exit(EXIT_FAILURE);
  }

  /* Register SIGSEGV handler */
  struct sigaction act;

  act.sa_handler = &sigsegv_handler;
  sigfillset(&act.sa_mask);
  act.sa_flags |= SA_ONSTACK;

  if (sigaction(SIGSEGV, &act, NULL) == -1) {
    fprintf(stderr, "ERROR: Unable to instal signal handler.\n");
    exit(EXIT_FAILURE);
  }
}

int trigger_stack_overflow(int a, int b, int c, int d, int e) {
  return trigger_stack_overflow(a + 1, b + 1, c + 1, d + 1, e + 1) *
         trigger_stack_overflow(a - 1, b - 1, c - 1, d - 1, e - 1);
}

int main() {
  install_sigsegv_handler();

  if (sigsetjmp(stack_overflow_guard, 0) == 0) {
    printf("normal path, got: %d\n",
           trigger_stack_overflow(0, 0, 0, 0, 0));
  } else {
    printf("stack overflow\n");
  }

  return EXIT_SUCCESS;
}

2011年3月12日 星期六

libffi 的範例

在 C/C++ 當中,函式的型別必需在編譯期固定下來才可以正確的呼叫。所以 Function Pointer 都要記錄 Function 的 Signature,簡單地說下面二個 Function Pointer 是不同的型別:

void (*func0)();
void (*func1)(int);

然而在很多情況下,我們只能在執行期決定我們要呼叫的函數。最常見的例子就是有提供 Native Function Call 的 Interpreter。例如:Python (C API)、Java (JNI) 等等。然而這種功能如果只能使用 C 語言實作,會變得非常龐雜、沒效率而且失去一般性。所以有了 libffi 的產生。

libffi 的全名是 Foreign Function Interface,他本身是用組合語言撰寫,不過他提供了一個 Portable 的 API。我們只要呼叫這些 API 就可以進行動態的函式呼叫(這和 Function Pointer 不同。如前所述,Function Pointer 必需在 Compiler Type 決定函式的型別。而 libffi 可以在執行期決定 Callee 的型別)。許多有名的計劃都是 libffi 的使用者,例如:CPythonOpenJDKDalvik VM 等等。

以下是簡單的範例程式:

#include <stdio.h>
#include <stdlib.h>

/* libffi 的 header */
#include <ffi.h>

/* 一些亂七八糟的函式 */
int func0() {
  printf("%s()\n", __func__);
  return 0;
}

int func1(int p0) {
  printf("%s(%d)\n", __func__, p0);
  return p0;
}

int func2(int p0, int p1) {
  printf("%s(%d, %d)\n", __func__, p0, p1);
  return p0 + p1;
}

/* 函式查詢表。也可以使用 hash table、binary tree 來實作,
   不過作為一個簡單的範例直接使用 array 就好了。 */
void *func_table[] = {
  (void *)func0, (void *)func1, (void *)func2,
};

int main(int argc, char **argv) {
  int i;

  /* 決定我們要使用哪一個函式 */
  if (argc < 2) {
    fprintf(stderr, "USAGE: %s parameter-count\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  int parameter_count = atoi(argv[1]);

  if (parameter_count > 2) {
    fprintf(stderr, "WARNING: Parameter count is greater than 2.\n");
    parameter_count = 2;
  }

  /* 讀取函式的參數 */
  int values[2] = { 0 , 0 };

  for (i = 0; i < parameter_count; ++i) {
    if (scanf("%d", &values[i]) != 1) {
      static char const *const postfix[] = {
        "th", "st", "nd", "rd", "th",
        "th", "th", "th", "th", "th"
      };

      fprintf(stderr, "Unable to read %d%s parameter\n",
              i + 1, postfix[(i + 1) % 10]);
      exit(EXIT_FAILURE);
    }
  }

  /* 使用 libffi 呼叫指定的函式 */
  ffi_cif cif;
  ffi_type *args[] = { &ffi_type_sint, &ffi_type_sint };
  void *ptr_values[] = { &values[0], &values[1] };

  /* 準備 ffi 所需的資料結構 */
  if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, parameter_count,
                   &ffi_type_sint, args) == FFI_OK) {
    int sum = 0;

    /* 執行函式呼叫 */
    ffi_call(&cif, func_table[parameter_count], &sum, ptr_values);

    printf("return value: %d\n", sum);
  } else {
    fprintf(stderr, "Unable to initialize FFI structure.\n");
    exit(EXIT_FAILURE);
  }

  return EXIT_SUCCESS;
}

2011年2月12日 星期六

幫 C 語言加上 Garbage Collector

前幾天讀 Dragon Book 的時候突發奇想,何不自己實作 Conservative Garbage Collector?

一般來說 Tracing Garbage Collector 分成以下三個步驟:
  1. 先從 call stack 或者 global variables 的 references (或者 pointers) 蒐集 object 的位址作為 root set,並標記 root set 當中的 object。
  2. 檢視 root set 當中的 object。如果 object 也包含 reference,而且這個 reference 指向的 object' 沒有被標記過,就標記並檢視 object'。(不斷遞迴直到沒有新的 object 被標記)
  3. 清除並回收沒有被標記的 object。
然而為了有效率地計算 root set 與走訪被標記的 objects,通常我們會需要編譯器加入一些額外的資訊。而且程式語言必需是 type-safe 的!著名的例子如 Java 或者是 Haskell、Common Lisp 等等。而 C/C++ 就不適合當 Garbage Collector 的目標語言,因為 C/C++ 支援 Pointer Arithmetic、Union Type 等語言構件,使得我們幾乎不可能在 C/C++ 之上實作 (Accurate) Garbage Collector。

不過 Conservative Garbage Collector 的做法不太一樣。Conservative Garbage Collector 的想法是:我不去分辨誰是 Pointer Type 誰是 Integer Type,只要「看起來」像是 Pointer 的 bit pattern,我就把他當成 Pointer。這有一個好處:我們不需要編譯器的幫忙也可以寫 Garbage Collector,所以可以和其他的程式碼混用;然而也有一個壞處:有時候 Conservative Garbage Collector 過於保守,以至於無法回收一些可以回收的記憶體。以下我就以一個小程式展現 Conservative Garbage Collector 的基本概念。

首先我們的第一個問題是如何找到 root set?我們可以取 main 函式 argc 的位址,另外在 gc_cleanup_ 函式隨意傳入一個整數,再取該整數的位址。所有還可能會用到的 pointer 就會在這二個整數之間:

void *stack_top;

void gc_init(int *argc_ptr) {
  stack_top = argc_ptr;
}

void gc_cleanup_(int stack_indicator) {
  void *stack_bottom = &stack_indicator; 
  /* Stack 的有效範圍就介於 [stack_bottom, stack_top] 之間 */
}

int main(int argc, char **argv) {
  gc_init(&argc);
}

當然我們還必需考慮 alignment 的問題,所以要把 stack_top 與 stack_bottom 對齊到 sizeof(void *) 的某倍。這可以用以下二個巨集來達成:

#define FLOOR_ALIGN_TO_WORD(X) \
    (((uintptr_t)(X)) / sizeof(void *) * sizeof(void *))

#define CEIL_ALIGN_TO_WORD(X) \
    (((uintptr_t)(X) + sizeof(void *) - 1) / \
        sizeof(void *) * sizeof(void *))

接著我們要準備一些空間來當我們的 Heap,所以我們在 gc_init 之中加入一些程式碼:

static char *heap_begin = NULL;
static char *heap_free = NULL;
static char *heap_end = NULL;

/* Initialize the internal data structure of the
   garbage collector */
void gc_init(int *argc_ptr) {
    /* We assume that argc is the variable in the
       stack having the highest address. */
    stack_top = (void **)CEIL_ALIGN_TO_WORD(argc_ptr);

    /* Allocate the heap using mmap. */
    heap_begin = (char *)mmap(0, GC_HEAP_SIZE,
                              PROT_READ | PROT_WRITE,
                              MAP_PRIVATE | MAP_ANONYMOUS,
                              -1, 0);

    if (!heap_begin || heap_begin == MAP_FAILED) {
        fprintf(stderr,
                "Unable to allocate the heap (size=%lu).\n",
                (unsigned long)GC_HEAP_SIZE);
        exit(EXIT_FAILURE);
    }

    heap_free = heap_begin;
    heap_end = heap_begin + GC_HEAP_SIZE;
}

接著我們需要一些資料結構用以管理我們分配出去的記憶體。做為一個簡單的範例,我只用了一個陣列依序記錄分配出去的記憶體,我的陣列會從 heap_end 開始往 heap_begin 的方向生長。

當然這是一個很沒有效率的作法,比較有效率的方法是使用類似 buddy system 之類的資料結構來管理 heap。

enum {
    ALLOC_STATUS_UNKNOWN,
    ALLOC_STATUS_TOUCHED,
    ALLOC_STATUS_REFERRED,
    ALLOC_STATUS_DEALLOCATED,
};

typedef struct alloc_record_ {
    char *addr;
    size_t size;
    unsigned int status;
} alloc_record;

為了操作 alloc_record 資料結構,我寫了三個函式,其功能分述如下(程式碼就不再贅述):

static alloc_record *find_alloc_record(char *addr) {
  /* 給定一個位址,找出它是屬於哪一次 allocation */
}

static void insert_alloc_record(char *addr, size_t size) {
  /* 在 alloc_record 的尾端加上一筆記錄 */
}

static void *allocate_deallocated_block(size_t size) {
  /* 重新把 deallocated 過的位址配置給不同的人 */
}

接下來是 Malloc 的程式碼。我們先嘗試直接從 heap_free 配置記憶體,如果記憶體不夠,再嘗試使用別人 deallocate 的記憶體。如果以上二者都沒有用,我們就必需進行 Garbage Collection。(備註:gc_cleanup(); 即 gc_cleanup_(0);)

/* Allocate a block with given bytes. */
void *gc_malloc(size_t size) {
    /* Make sure that all of returned address is aligned */
    size = CEIL_ALIGN_TO_WORD(size);

    /* Try to allocate if free space available */
    if (is_free_space_avail(size)) {
        char *result = heap_free;
        heap_free += size;
        insert_alloc_record(result, size);
        return result;
    }

    /* Try to allocate from deallocated address. */
    void *result = allocate_deallocated_block(size);
    if (result) {
        return result;
    }

    /* Try to collect garbage and retry. */
    while (gc_cleanup()) {
        void *result = allocate_deallocated_block(size);
        if (result) {
            return result;
        }
    }

    return NULL;
}

當然有 Malloc 就要有 Free,不過值得注意的是:為了提高 Conservative Garbage Collector 的回收率,減少「應該 deallocate 而沒有 deallocate」的情況,我們應該要抹除整個 object 與傳入的 Pointer

/* Deallocate and wipe the allocated block. */
static void deallocate(alloc_record *record) {
    /* Mark this block as deallocated */
    record->status = ALLOC_STATUS_DEALLOCATED;

    /* Wipe the memory block for cleaning the possible
       pointer address */
    memset(record->addr, '\0', record->size);
}

/* Deallocate the given address and wipe the pointer. */
void gc_free_(void **addr_ptr) {
    alloc_record *record = find_alloc_record(*addr_ptr);
    if (!record) {
        fprintf(stderr, "Memory corrupted\n");
        return;
    }

    deallocate(record);
    *addr_ptr = NULL;
}

最後就是最關鍵的 Garbage Collection 的程式碼 gc_cleanup

/* Garbage collection and deallocate unused memory. */
size_t gc_cleanup_(int stack_indicator) {
    alloc_record *records =
        (alloc_record *)heap_end - alloc_record_count;

    size_t i, j;

    /* Mark alloc record as UNKNOWN */
    for (i = 0; i < alloc_record_count; ++i) {
        if (records[i].status != ALLOC_STATUS_DEALLOCATED) {
            records[i].status = ALLOC_STATUS_UNKNOWN;
        }
    }

    /* Scan the stack */
    void **stack_bottom =
        (void **)CEIL_ALIGN_TO_WORD(&stack_indicator);
    assert(stack_top != NULL);
    assert(stack_bottom <= stack_top);
    scan_and_touch(stack_bottom, (void **)stack_top);

    /* Scan the heap */
    scan_touched_objects();

    /* Deallocate and reset the status */
    static unsigned int gc_count = 0;
    static char const sep[] =
    "-------------------------------------------------------";

    fprintf(stderr, "%s\n", sep);
    fprintf(stderr,
            "GARBAGE COLLECTION ROUND #%u\n", ++gc_count);

    size_t deallocated_count = 0;
    for (j = alloc_record_count, i = j - 1; j > 0; --i, --j) {
        if (records[i].status == ALLOC_STATUS_UNKNOWN) {
            fprintf(stderr,
                    "  Reclaim [addr: %p, size: %lu]\n",
                    records[i].addr,
                    (unsigned long)records[i].size);

            deallocate(&records[i]);
            ++deallocated_count;
        }
    }

    fprintf(stderr, "%s\n\n", sep);

    return deallocated_count;
}

/* Scan for address between [begin, end) */
static void scan_and_touch(void *begin_, void *end_) {
    void **begin = (void **)FLOOR_ALIGN_TO_WORD(begin_);
    void **end = (void **)FLOOR_ALIGN_TO_WORD(end_);

    for (; begin < end; ++begin) {
        char *addr = (char *)*begin;

        if (addr >= heap_begin && addr < heap_free) {
            alloc_record *record = find_alloc_record(addr);

            if (record && record->status ==
                    ALLOC_STATUS_UNKNOWN) {
                record->status = ALLOC_STATUS_TOUCHED;
            }
        }
    }
}

/* Scan the touched objects */
static void scan_touched_objects() {
    alloc_record *records =
        (alloc_record *)heap_end - alloc_record_count;

    size_t count, i;

    do {
        count = 0;
        for (i = 0; i < alloc_record_count; ++i) {
            if (records[i].status != ALLOC_STATUS_TOUCHED) {
                /* Not in touched state, skip it. */
                continue;
            }

            /* Mark as referred. */
            records[i].status = ALLOC_STATUS_REFERRED;

            /* Scan this object */
            scan_and_touch(
                (void **)(records[i].addr),
                (void **)(records[i].addr + records[i].size));

            count++;
        }
    } while (count > 0);
}

以上程式碼就是一個具體而微的 Conservative Garbage Collector,完整的程式碼與測試程式可以從這裡下載:conservativegc.h , conservativegc.c , test_tree.c , test_many.c 。當然這個 Conservative Garbage Collector 還少了很多東西:包括計算 static storage 的 root set(在 Linux 之下可以檢查 etext 與 end 二者之間的值),還有更有效率的 allocation policy 等等。

備註:如果你真的有在 C/C++ 使用 Garbage Collector 的需求可以參考 libgc,一個由 BoehmDemersWeiser 等前輩撰寫 (他們是提出 Conservative Garbage Collector 的重要前輩),並被移植到多個平台的 Conservative Garbage Collector 函式庫。