関数を作る

関数を実行時に作る

今回は関数を作ってみましょう。作るといっても、コンパイル時に作るのではありません。実行時に作ります。

全体の流れとしては、「①関数を置く領域を作る」→「②そこに処理を書き込む」→「③呼び出す」となります。今回は②のところで「4バイト整数を受け取り、これに100を足した数を返す」とすることにしましょう。

①関数を置く領域を作る

変数の型には、「int」や「double」や「char *」などの「1つの値を保管するもの」と、「int[]」や「char[]」や構造体などの「複数の値を保管するもの」があります。関数のこなす処理はそのどちらで表されるしょうか。もちろん、複雑な処理が4バイトや8バイトで表されるわけがありませんから、後者になります。関数はたくさんの小さな処理の積み重ねでできていて、1つ1つの処理は1〜7バイト程度で表されます。関数を作るときには、これらを連続したデータ領域に書き込む必要があります。

ここで注意しなければいけないことがあります。普段連続したデータ領域を作るときには配列を作ったりmalloc/calloc関数を使ったりしますが、関数の処理を書き込む領域をこのやり方で確保しても、呼び出して実行することができません(※環境による)。ハッキング防止のためにOSが実行禁止にしているからです。

そこで、別のやり方でデータ領域を確保する必要があります。mmap関数というものを使います。

mmap関数は、malloc関数を使ってある程度の大きさのデータ領域を要求したときに内部的に呼び出される関数です。簡単に言うと、動的にデータ領域を確保する手段の1つということです。

#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);

mmap関数の第3引数protで、そのデータ領域に対するどんなアクセスの仕方を許可するか指定できます。PROT_READは読み出しの許可、PROT_WRITEは書き込みの許可、PROT_EXECは実行の許可です。mallocを使うとこのPROT_EXECが指定されないため、実行できないのです。PROT_READ、PROT_WRITE、PROT_EXECのうち複数を指定したいときは | でビット和をとります。何も指定しないときはPROT_NONEとします(データにアクセスできなくなります)。

さて、今回は「関数の処理を書き込む」と「実行する」の両方が必要ですから第3引数protには「PROT_WRITE | PROT_EXEC」を指定します。

また、第4引数では実際にどの場所にメモリ領域を確保するのか指定します。例えばここで"hoge"という名前のファイルを作ってそのディスクリプタを渡すと、書き込んだ内容がhogeに出力されるのですが、今回は関数を作って使うだけでその中身をファイルに残しておく必要が無いので /dev/zero を使います。open("/dev/zero", O_RDONLY);の返り値を渡せばよいです。
2018/12/04追記:O_RDWRとしていましたがO_RDONLYで良いようです。

第1引数addrはNULL。第2引数lenは確保するバイト数。第4引数flagsはMAP_PRIVATE。第6引数offは0とします。

mmap関数は確保した領域の先頭アドレスを返すので、それをchar *型の変数に入れます。これで関数を置く領域が作れました。

また、malloc関数で確保した領域をfree関数で解放するのと同じように、mmap関数で確保した領域は使い終えた後にmunmap関数という関数を使って解放します。munmap関数の第1引数には確保する領域のアドレス、第2引数には確保していたバイト数を渡します。

ここまでの流れをまとめて書くと次のようになります。

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main(void){
    int fd, mapsize;
    char *map;
    fd = open("/dev/zero", O_RDONLY);
    mapsize = 100;
    map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    close(fd);
    munmap(map, mapsize);
    return 0;
}

確保する領域のサイズmapsizeは一時的に100としておきましたが、作る関数に合わせて後で変更します。

②処理を書き込む

確保した領域に実際に処理を書き込んで関数を作ります。

今回は4バイトの引数を1つ受け取り100を足して返す関数なので、「push %rbp」(55)「mov %rsp,%rbp」(48 89 e5)「mov %edi,-0x4(%rbp)」(89 7d fc)「mov -0x4(%rbp),%eax」(8b 45 fc)「add $0x64,%eax」(83 c0 64)「pop %rbp」(5d)「retq」(c3)となりますね。これをconst charの配列として用意しておいて、memcpy関数を使ってさっき作った領域にコピーします(memcpy関数を使うにはstring.hをインクルードします)。

const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
memcpy(map, data, sizeof data);

これで関数が作れました!!!

③呼び出す

mapはchar *型なので、そのままでは関数として呼び出すことができません。関数ポインタ型という型にキャストする必要があります。

fncという名前の関数ポインタ型の変数を宣言するときは次のようにします。

int型の引数を3つ受け取ってint型の値を返す関数へのポインタ:

int (*fnc)(int, int, int);

引数を受け取らずdouble *型の値を返す関数へのポインタ:

double *(*fnc)(void);

第1引数がchar[]型、第2引数がint型で、値を返さない関数へのポインタ:

void (*fnc)(char [], int);

今回はint型の引数を1つ受け取ってint型の値を返すので、int(*fnc)(int)となります。mapをキャストしてこれに代入します。

int (*fnc)(int);
fnc = (int(*)(int))map;

そのまま初期化するなら

int (*fnc)(int) = (int(*)(int))map;

のようになります。

(※mapを初めからvoid *型にしておけばキャスト演算子を書く必要は無くなります。またmapという変数を使わずに初めからfncだけを宣言しても構いません。今回は何をしているのか明確にするため書いています。)

さて、準備は整いました。いよいよ作った関数を呼び出します。

fnc(3);

試しに表示してみると、ちゃんと100が足されて返ってくるはずです。

printf("fnc(3) = %d\n", fnc(3));

出力:

fnc(3) = 103

ここまでをまとめると、コード全体は次のようになります:

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>

int main(void){
    int fd, mapsize;
    char *map;
    const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
    int (*fnc)(int);
    fd = open("/dev/zero", O_RDONLY);
    mapsize = sizeof data;
    map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    memcpy(map, data, sizeof data);
    fnc = (int(*)(int))map;
    printf("fnc(3) = %d\n", fnc(3));
    close(fd);
    munmap(map, mapsize);
    return 0;
}

ちょっと簡潔にしたバージョン:

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>

int main(void){
	const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
	int fd = open("/dev/zero", O_RDONLY);
	int (*fnc)(int) = mmap(NULL, sizeof data, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
	memcpy(fnc, data, sizeof data);
	printf("fnc(3) = %d\n", fnc(3));
	close(fd);
	munmap(fnc, sizeof data);
	return 0;
}

応用編

これを応用すると、「関数を返す関数」を作ることもできます。実質関数のカリー化(?)

「unsigned型変数nを受け取り、「2つのdouble型変数a, bを受け取り、aにbをn回かけた数を返す関数」を返す関数」を繰返2乗法で作ります。関数の宣言は次のようになります。

double (*make_pow(unsigned))(double, double);

C言語ってこんなこともできるんですね。でも考えてみれば結局返してるのは単なるポインタか。

実装は次のようになります。本当は確保したメモリを解放するための関数も別途作るべきなのですが面倒なので省きます。

double (*make_pow(unsigned n))(double, double){
    int fd = open("/dev/zero", O_RDONLY);
    const char data1[] = {0x55, 0x48, 0x89, 0xe5, 0xf2, 0x0f, 0x11, 0x45, 0xf8, 0xf2, 0x0f, 0x11, 0x4d, 0xf0}, data2[] = {0xf2, 0x0f, 0x10, 0x45, 0, 0xf2, 0x0f, 0x59, 0x45, 0xf0, 0xf2, 0x0f, 0x11, 0x45, 0}, data3[] = {0xf2, 0x0f, 0x10, 0x45, 0xf8, 0x5d, 0xc3};
    int mapsize = 6;
    for(unsigned i = n; i; i >>= 1) mapsize += (i & 1 ? 30 : 15);
    char *map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    memcpy(map, data1, sizeof data1);
    int cursor = sizeof data1;
    if(n & 1){
        memcpy(map + cursor, data2, sizeof data2);
        map[cursor + 4] = map[cursor + 14] = 0xf8;
        cursor += sizeof data2;
    }
    n >>= 1;
    while(n){
        memcpy(map + cursor, data2, sizeof data2);
        map[cursor + 4] = map[cursor + 14] = 0xf0;
        cursor += sizeof data2;
        if(n & 1){
            memcpy(map + cursor, data2, sizeof data2);
            map[cursor + 4] = map[cursor + 14] = 0xf8;
            cursor += sizeof data2;
        }
        n >>= 1;
    }
    memcpy(map + cursor, data3, sizeof data3);
    return (double(*)(double, double))map;
}

この関数に例えば11を渡すと、

double fnc(double a, double b){
    a *= b;
    b *= b;
    a *= b;
    b *= b;
    b *= b;
    a *= b;
    return a;
}

と同じ関数が返ります。実用性はそんなに無いんですがちょっと面白い。

この記事があなたの参考になれば幸いです。ではまた