OpenBSDのexplicit_bzero(3)の仕組み

OpenBSDのlibcにはexplicit_bzero(3)という関数があって、変数を使い終わった後にmemset(3)で0クリアできるようにする関数。
これはコンパイラがmemset()後にその変数が使われないならmemset()自体いらないだろうという最適化によってmemset()自体が消されるのを防いでいます。

これの仕組みはコミットログにて解説されているんだけど、内容は至って簡単だけどなるほど〜と思うわけですね。
コードはこれです。

/*  $OpenBSD: explicit_bzero.c,v 1.3 2014/06/21 02:34:26 matthew Exp $ */
/*
 * Public domain.
 * Written by Matthew Dempsky.
 */

#include <string.h>

__attribute__((weak)) void
__explicit_bzero_hook(void *buf, size_t len)
{
}

void
explicit_bzero(void *buf, size_t len)
{
    memset(buf, 0, len);
    __explicit_bzero_hook(buf, len);
}

仕組みとしてはmemset()の呼び出し後に何もしない関数を呼んでいるんだけど、これはweakシンボルが付いていてコンパイル時点では何を呼ぶのか決められないようになっているというのがポイントでしょうね。__explicit_bzero_hook()がコンパイル・リンク時に決まらないのでこれについては最適化ができないのでコンパイラはmemset()を消すことができないというところでしょう。

では、ちょっと試してみます。

まずはmemset_optimized.c。

#include <stdio.h>
#include <string.h>

void clear(void *buf, size_t len)
{
    memset(buf, 0x0, len);
}

int main(int argc ,char **argv)
{
    char buf[256] = { 0 };

    strncpy(buf, argv[0], sizeof(buf) - 1);

    printf("argv[0]: %s\n", buf);

    clear(buf, sizeof(buf));

    return 0;
}

これを最適化のレベルを2にしてアセンブラまで出す。

$ gcc -Wall -O2 -S memset_optimized.c

こんな風になってprintf()後のclear()関数の呼び出しごと消えています。

main:
.LFB26:
    .cfi_startproc
    subq $264, %rsp
    .cfi_def_cfa_offset 272
    movq (%rsi), %rsi
    xorl %eax, %eax
    movq %rsp, %rdi
    movl $32, %ecx
    movl $255, %edx
    rep stosq
    movq %rsp, %rdi
    call strncpy
    movq %rsp, %rsi
    movl $.LC1, %edi
    xorl %eax, %eax
    call printf
    xorl %eax, %eax
    addq $264, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc

つぎにweak属性を付けたダミー関数を呼ぶ場合。

#include <stdio.h>
#include <string.h>

void __attribute__((weak)) dummy_func(void)
{
}

void clear(void *buf, size_t len)
{
    memset(buf, 0x0, len);

    dummy_func();
}

int main(int argc ,char **argv)
{
    char buf[256] = { 0 };

    strncpy(buf, argv[0], sizeof(buf) - 1);

    printf("argv[0]: %s\n", buf);

    clear(buf, sizeof(buf));

    return 0;
}

clear()自体はinline展開されてますがその後にmemset()らしき処理(コンパイラの最適化でmemset()を呼ばずに同様に処理に置き換わっているから)が行われた後にdummy_func()が呼ばれてます。

main:
.LFB27:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    pushq    %rbx
    .cfi_def_cfa_offset 24
    .cfi_offset 3, -24
    xorl %ebp, %ebp
    movq %rbp, %rax
    movl $32, %ecx
    movl $255, %edx
    subq $264, %rsp
    .cfi_def_cfa_offset 288
    movq (%rsi), %rsi
    movq %rsp, %rdi
    rep stosq
    movq %rsp, %rdi
    call strncpy
    movq %rsp, %rsi
    movl $.LC2, %edi
    xorl %eax, %eax
    call printf
    movq %rbp, %rax
    movq %rsp, %rdi
    movl $32, %ecx
    rep stosq
    call dummy_func
    addq $264, %rsp
    .cfi_def_cfa_offset 24
    xorl %eax, %eax
    popq %rbx
    .cfi_def_cfa_offset 16
    popq %rbp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc

ちなみに、コンパイルオプションで-fno-builtinを使うとmemset()が呼ばれるようになります。

main:
.LFB27:
    .cfi_startproc
    subq $264, %rsp
    .cfi_def_cfa_offset 272
    movq (%rsi), %rsi
    xorl %eax, %eax
    movq %rsp, %rdi
    movl $32, %ecx
    movl $255, %edx
    rep stosq
    movq %rsp, %rdi
    call strncpy
    movq %rsp, %rsi
    movl $.LC2, %edi
    xorl %eax, %eax
    call printf
    movq %rsp, %rdi
    movl $256, %edx
    xorl %esi, %esi
    call memset
    call dummy_func
    xorl %eax, %eax
    addq $264, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc