読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

64bit Linuxでアセンブリで書いたForthからC言語の関数を呼ぶ

asm forth

ForthからSDLやAllegroを使って何かやりたいな、と思い立ったんだけど、基礎知識が無いので調べるのに苦労した。ので、記録しておく。

結論としては

  • gcc-ldlフラグを追加し、dlopen/dlsymを使えるようにする
  • スタックから各引数のレジスタにpopしてcallする

だった。

以下イチから調べた過程も併せて書いておく。順番は

  1. C言語から動的ライブラリを実行時に読み込み、そのライブラリの関数を呼ぶ
  2. アセンブリで同じことをする
  3. Forthから呼び出せるようにネイティブワードを追加

というカンジで試した。

C言語から動的ライブラリを実行時に読み込み、そのライブラリの関数を呼ぶ

調べた結果DLOPENを使えば達成できるとわかったので、サンプルコードを改造して、Allegro5のライブラリを呼び出して使ってみた。

コードgithubに上げたけど、一応ここにもソースを書いておく。

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

int main(int argc, char **argv) {
  void *handle;
  int (*al_init)(int, int *(void (*)(void)));
  int *allegro_version;
  int success;
  char *error;
  
  handle = dlopen("/usr/lib/x86_64-linux-gnu/liballegro.so", RTLD_LAZY);
  
  if (!handle) {
    fputs (dlerror(), stderr);
    exit(1);
  }
  
  al_init = dlsym(handle, "al_install_system");
  if ((error = dlerror()) != NULL)  {
    fputs(error, stderr);
    exit(1);
  }

  // ALLEGRO_VERSION_INT = 83888897 (liballegro 5.0.11)
  success = (*al_init)(83888897, NULL);
  if (success) {
    printf ("success: %d\n", success);
  }
  
  dlclose(handle);
}

ALLEGRO_VERSION_INT#defineされていて、ヘッダファイルをインクルードしないと使えないので、調べてそのまま書いた。

手順としては

  1. dlopenで動的ライブラリファイル(.so)のパス、フラグを指定してハンドルを取得する
  2. dlsymで、ハンドルとシンボル名を指定して関数又は変数のアドレスを取得する
  3. 取得した関数のアドレスを使って呼び出す
  4. dlcloseで閉じる(どの時点で閉じていいか調べてない)

となる。

アセンブリで同じことをする

C言語の関数を呼ぶ事自体始めてやったので、いろいろ知る必要があった。

  • C言語の関数はそのままcallして呼べる
  • 引数はx64 Assembly Language Programmingを参考。各レジスタに割り当てて使う
  • ポインタはそのままアドレスを渡せばとりあえず使える
  • gccをリンカに使う場合はエントリポイントをmainにする

nasmとgccを使う場合、次のコマンドでビルドできる。

nasm -f elf64 -l main.lst  main.asm
gcc -o main main.o -ldl

64bit nasmからprintfを使うサンプルがあったので、まずそれを参考に、レジスタの割り当てを確認した。

コード

次に上のC言語の実験コードと同じことをやった。エラーチェックは面倒なので書いていない。

コード

(なんかインデントぐちゃぐちゃになってる。。)

これも一応載せておく。

    extern  dlopen
    extern  dlsym
    extern  dlclose
    extern  printf
    
    SECTION .data

allegro:         db "/usr/lib/x86_64-linux-gnu/liballegro.so", 0
allegro_handle:  dq 0

al_install: db "al_install_system", 0
al_init:    dq 0
al_success: dq 0

fmt:    db "success: %d", 10, 0
    
    SECTION .text
    
    global main       ; GCC用のエントリポイント
main:
    push    rbp       ; スタックフレーム保存
    
    ;; ライブラリopen
    mov rdi, allegro            ; lib
    mov rsi, 1                  ; flag (RTLD_LAZY)
    xor rax, rax                ; SSE(0)
    call dlopen
    mov [allegro_handle], rax

    ;; init読み込み
    mov rdi, [allegro_handle]
    mov rsi, al_install
    xor rax, rax
    call dlsym
    mov [al_init], rax

    ;; al_init
    mov rbx, [al_init]
    mov rdi, 83888897
    mov rsi, 0
    xor rax, rax
    call rbx
    mov [al_success], rax

    ;; 結果
    mov  rdi,fmt
    mov  rsi,[al_success]         ; 2
    xor rax, rax
    call    printf
    
    ;; ライブラリclose
    mov rdi, [allegro_handle]
    xor rax, rax                ; SSE(0)
    call dlclose
    
    pop  rbp                     ; スタックフレーム復帰
    mov  rax,0                  ; mainのreturnの返り値
    ret

一応実行したら、関数を呼び出せていることを確認できる。al_install_systemに渡すバージョンを変えると初期化失敗の0が返ってくる。

Forthから呼び出す

最後に、自作Forthから呼び出すコードを追加した。

本体コード

dlopen関連だけは予め組み込んでおく必要がある。

  • externでdlopenなどを使えるようにする
  • リンカをgccに変更して-ldlフラグを与える
  • gcc用にエントリポイントを_startからmainに変更する
  • dlopenなどを呼び出すワードと、アドレスからC関数を呼び出すワードを追加する

あたりを追加・変更した。

参考に、dlopenを呼び出すワードは次のような定義になった。

    ;; ( lib flag -- handler )
    defcode "dlopen", 0, DLOPEN
    mov rcx, rsi                ; rsiの保存
    pop rsi                     ; 第二引数flag
    pop rdi                     ; 第一引数lib(&CStr)
    push rcx
    xor rax, rax                ; SSE未使用
    call dlopen
    pop rsi
    push rax
    NEXT

rsiをインストラクションポインタとして使っているので、破壊されないように保存しておく。

C言語の関数を呼び出すワードは、次のように引数の数を固定するものにした。32bitだとスタックを引数渡しに使うみたいなので、そのまま渡せて楽そうだ。(アライメントが違うかもだけど)

    ;; ( arg1 arg2 &func -- result )
    defcode "c-funcall-2", 0, CFUNCALL2
    mov rcx, rsi                ; rsiの保存
    pop rbx                     ; &func
    pop rsi                     ; arg2
    pop rdi                     ; arg1
    push rcx
    xor rax, rax                ; SSE未使用
    call rbx
    pop rsi                     ; rsi復元
    push rax                    ; 結果
    NEXT

ネイティブワード定義によって、以下のように実行時にライブラリを読み込んでC関数を呼び出せるようになった。ex-cfuncを実行するとSegmentation Faultになっちゃうんだけど、初期化自体は成功してるっぽいので、その後の処理が何か関係してるのかもしれない。とりあえず今日はここで力尽きた。

private/
  CREATE handle cell allot
  CREATE alinst cell allot

  : ex-dlopen  ( -- )
     " /usr/lib/x86_64-linux-gnu/liballegro.so" >cstr 1 dlopen  handle ! ;
  : ex-dlsym  ( -- )
     " al_install_system" >cstr handle @ dlsym  alinst ! ;  
  : ex-cfunc  ( -- )
     83888897 0 alinst @  c-funcall-2 ;
  : ex-dlclose  ( -- )
     handle @ dlclose ;

  : ?success  ( ? -- )  ( dlcloseはスタックトップが0なら成功 )
     if return then  ." success!" ;
reveal>>
: dl-example  ( -- r )
   ex-dlopen
   ex-dlsym
   ( ex-cfunc  ." init:" . )  ( segfault )
   ex-dlclose ?success ;
/private

おわりに

低レイヤのこと全く知らないから、どこから調べていいのかわからなくて、心理的な壁が高くてなかなか手を付けられなかった。最終的にエイヤッと気合を入れて調べたらわかったので良かった。

が、モチベーションを期待するのはあんまり賢いとは思えない。先に基礎的な事を網羅した本などに目を通しておいて、「アレをやりたいな……アレとアレを使えばいけそうだ」みたいに、ぼんやりとでも方向性が掴めればもっと早く取り掛かれてた気がする。

広告を非表示にする