無限遠まで突撃中

ネット日記書きの徒然。

自作OSのmallocを大きなバイト数でアライメントできるようにしたい

こんにちは、突撃隊です。

コロナで皆さん大変ですが、自分は作業がはかどっています。 なんとも皮肉なもんです。

自作OSのmallocで大きなアライメントを取りたい

タイトルのとおりです。 4KBとか1MBとかのアライメントを取りたいねという話です。

自分の自作OS「minOSv2」では動的メモリ確保にK&R mallocを実装しています。

github.com

mallocの基本構造は らじうむ覚書さんの K&R Malloc解説 を読むと良いです。

以下の文章ではK&R mallocの基本概念はわかっているとします。

mallocのヘッダサイズと16バイトアライメント

minOSv2のmallocヘッダは以下のように定義されています。

minOSv2/mm.h at feature-ext2 · Totsugekitai/minOSv2 · GitHub

struct malloc_header {
    struct malloc_header *next;
    uint64_t size; // This param's unit is sizeof(struct malloc_header).
};

ポインタ型と uint64_t 型の構造体なので、構造体のサイズは16バイトです。

K&R mallocはヘッダのサイズを1単位として扱うので、つまりmallocの単位としては16バイトずつの確保になります。

リンカスクリプトでヒープ領域をアライメントしてあげると、mallocで確保された領域は自動的に16バイトアライメントされていることになります。

16バイトより大きなアライメントを取るには

リンカスクリプトで調整してあげることによって16バイトアライメントが自動的になされることがわかりました。

しかし16バイトより大きいアライメントを取りたいときがあります。

そう、 ハードウェアの操作 をするときです。

AHCIの操作

この前までAHCIデバイスドライバを書くために奮闘していました。

totsugekitai.hatenablog.com

totsugekitai.hatenablog.com

AHCIデバイスドライバを書く際には128Bアライメントや4KBアライメントでメモリを確保する必要があります。

mallocくんは16バイトアライメントは守ってくれますが、それより大きなアライメントは対応していませんので、なんとかしてやる必要があります。

大きなアライメントに対応したメモリ確保をする関数を作る

まず、アライメントを調整するために何バイトmallocする必要があるでしょうか。

size バイトを al バイトでアライメントするときを考えると、 size + al バイト確保してやり、この確保したメモリ上で開始アドレスをいじってやればアライメントを守ったメモリが確保できそうです。

つまりこうすれば解決ですね。

void *not_aligned_allocate = malloc(size + al);
void *aligned_allocate = align(not_aligned_allocate, al);

void *align(void *addr, int align)
{
    uint64_t addr_num = (uint64_t)addr;
    return (void *)(addr_num + (align - (addr_num % align)));
}

freeができない

しかしこのままでは問題があります、freeができないのです。

というのもK&R Mallocのfreeは、引数で受け取ったアドレスの一個手前にmallocヘッダがあると仮定した処理をしています。

今私たちはmallocしたときに返ってきたアドレスを何バイトか後ろにずらして使っていますので、こうすることはできないのです。

free(aligned_allocate);

何バイトずらしたか情報をもたせたい

何らかの手を考える必要があります。

私たちが知りたいのは何バイトずらしたかという情報です。 この情報さえあれば、大きくアライメントされていても元に戻すことができます。

ここで考えてみると、mallocは16バイトごとにメモリを確保し、そして16バイトより大きくアライメントされているときには、 最低でも16バイトは元の確保したメモリの先頭アドレスからずらしているはずです(ここでは2の累乗バイトアライメントを考えています)。

ということはアライメントした後のアドレスからアドレスが小さい方向に16バイトは何にも使われないことになります。

そういうわけでここに情報を書き込んじゃいましょう。

こちらがソースコードになります。

void *kmalloc_alignas(int size, int align_size)
{
    void *tmp = kmalloc(size + align_size);
    if (is_aligned(tmp, align_size)) {
        return tmp;
    } else {
        void *true_ptr = align(tmp, align_size);
        uint64_t diff = (uint64_t)true_ptr - (uint64_t)tmp;
        putsn_serial("malloc align diff: ", diff);
        ((uint64_t *)true_ptr)[-1] = diff;
        strcpy(&((char *)true_ptr)[-16], "ALIGNED");
        return true_ptr;
    }
}

void kfree_aligned(void *ptr, int align_size)
{
    if ((unsigned long)align_size <= sizeof(struct malloc_header)) {
        kfree(ptr);
    } else {
        if (strcmp(&((char *)ptr)[-16], "ALIGNED") == 0) {
            uint64_t diff = ((uint64_t *)ptr)[-1];
            void *true_ptr = (void *)((char *)ptr - diff);
            putsn_serial("kfree addr diff: ", diff);
            putsp_serial("kfree true free address point: ", true_ptr);
            kfree(true_ptr);
        } else {
            kfree(ptr);
        }
    }
}

kmalloc_alignasとkfree_alignedという関数を作りました。

まずkmalloc_alignasでは、kmallocで確保したメモリ領域のアライメントがあっているかチェックし、あっていたらそのままリターンします。 あっていなかったら、アライメントした後のアドレスの前8バイトにアライメントのために移動させた差の値を書き込みます。

またアライメントした後のアドレスの前16バイトから前9バイトにかけて "ALIGNED" という識別子を置きました。

kfree_alignedでは、まずアライメントサイズを確認し、16バイト以下だったらずらされていないので普通にfreeします。

16バイトより大きい場合、アライメントのためにずらされているかどうかを "ALIGNED" の識別子があるはずの部分を見て判断します。 識別子があったらずらされていると判断し、アライメントのためにどれだけずらされたかを差の値があるはずの部分を読んで計算し直し、mallocヘッダが読み込まれるようにアドレスを調整してからfreeします。

いかがでしたか?

アライメントはたいへんですね、そういえばアライメントの話だとこんな記事もあります。

uchan.hateblo.jp

いや〜アライメントはたいへんですね、気をつけましょう。