C言語で文字列を連結・結合する: strcat, snprintf, 安全なstrcat

345, 2021-11-14

目次

C言語で文字列を連結する

C言語では文字列を扱うことができます。
文字列の末尾に別の文字列を連結したい場合があります。
そういう時にどうすればいいのかこの記事で解決します。

文字列の連結はさまざまなシーンで必要とされる処理です。
C言語の文字列はかなり低レイヤ―な構造のため、セキュリティを気にする場合は注意が必要です。
この記事ではセキュリティ面からも検証していきます。

具体的には↓を見ていきたいと思います。

  • C言語の文字列の連結の原理

  • strcat()の使い方

  • strcat()は安全か?

  • snprintf()の使い方

  • 安全なstrcatを作る

結論から言うと?

結論から言うと文字列の連結では極力snprintf()を使うようにしましょう。
処理速度ではstrcat()に劣りますが、安全性の面ではsnprintf()の方が上です。
strcat()はプログラムにバッファーオーバーランなどの脆弱性を生む可能性があります。

バッファーオーバーラン(バッファーオーバフロー)とはプログラムが確保されたバッファーのサイズを飛び越えてメモリにアクセスしてしまうことを言います。
確保されたメモリ領域の外側のメモリを書き換えてしまい、関数のreturnアドレスなどが変更されてしまう場合もあります。
この脆弱性を利用した攻撃が存在するため、攻撃されたプログラムは意図しない動作をしてしまう場合があります。

文字列の連結ではsnprintf()を多用するようにしてください。
もっとも、限られたケースではstrcat()の利用が適切な場合もあります。

C言語の文字列の連結の原理

C言語で文字列に別の文字列を連結させるには、関数を使います。
関数はstrcat()snprintf()などの関数です。
こういった関数はどのように文字列の末尾に別の文字列を連結させるのでしょうか?

C言語の文字列は文字の集まりです。
そして文字列の末尾には文字列の終端を表すナル文字がセットされています。
このナル文字が文字の終端部分です。

つまり文字列に別の文字列を連結させたい場合は、連結先の文字列のナル文字を後方に移動させる必要があることになります。
文字列のバッファが十分にある場合、文字列のナル文字部分から別の文字列の文字をセットして行って、最終的に別の文字列をセットし終えたらその時点の位置にナル文字をセットします。
これがstrcat()などの原理です。

strcat()の使い方

C言語で文字列を連結する方法の1つとして標準ライブラリのstrcat()を使う方法があります。
↓がサンプルコードです。

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

int main(void) {
    char s[100] = "Hello, ";

    strcat(s, "World!");  // 「Hello, 」の後ろに「World!」を連結

    printf("%s\n", s);  // Hello, World!

    return 0;
}

strcat()は第1引数のバッファの末尾にに第2引数の文字列を連結します。
↑の例で言うとsがバッファで「"World!"」が第2引数です。
strcat()は文字列を連結するとバッファの末尾にナル文字を付加します。

strcat()の第1引数のバッファのサイズは十分に確保されている必要があります。
もしバッファのサイズが十分でない場合は、バッファーオーバーランになる場合があります。

strcat()は返り値として第1引数のバッファのポインタを返します。

strcat()は安全か?

バッファのサイズが十分に存在し、かつ連結する文字列のサイズが把握できている場合はstrcat()の使用は適切と言えます。
しかし、それはプログラム的な安全性が除外されている状態です。
つまりプログラム的にバッファーオーバーランを防ぐ方法がありません
ヒューマンエラーが発生した場合はそのまま脆弱性が埋め込まれることになります。

そのためよっぽどの理由がある場合をのぞいて、strcat()ではなくsnprintf()の利用を検討したほうがいいです。
ただ、snprintf()使い方を間違えると脆弱性になる場合があるので、そういう意味ではC言語で開発しないのが一番安全なんですが。

悲しいこと言うなよ

snprintf()の使い方

文字列の連結にはsnprintf()という関数も使えます。
snprintf()を使ったサンプルコードは↓です。

#include <stdio.h>

int main(void) {
    const char *hello = "Hello, ";
    char dst[100];

    // 「Hello, 」の後ろに「World!」を連結する
    snprintf(dst, sizeof dst, "%sWorld!", hello);

    printf("%s\n", dst);  // Hello, World!
    return 0;
}

snprintf()は第1引数にバッファ、第2引数にバッファのサイズ、第3引数にフォーマット、第4引数以降にフォーマットの引数を取ります。
snprintf()はフォーマットとフォーマットの引数を合成して、バッファに保存します。
そのためこれを使った文字列の連結が可能です。

snprintf()バッファのサイズを指定できるため、バッファの容量を超えてバッファーオーバーランすることはありません。
ただし、バッファの実際のサイズと、指定したバッファのサイズが異なる場合はバッファーオーバーランになる可能性があります

snprintf()はフォーマットを工夫すれば文字列の末尾だけでなく、文字列の先頭にも別の文字列を連結できます。
そういった意味ではsnprintf()は文字列処理系の万能関数と言えます。

#include <stdio.h>

int main(void) {
    const char *world = "World!";
    char dst[100];

    snprintf(dst, sizeof dst, "Hello, %s", world);

    printf("%s\n", dst);  // Hello, World!
    return 0;
}

C言語の文字列処理で楽をしたい方はsnprintf()の使い方を覚えることをおすすめします。
この関数は色々なシーンで使うことができます。

安全なstrcatを作る

snprintf()は確かに便利なんですが、文字列の末尾に別の文字列を連結したい場合は少し大げさな気がします。
ここではstrcat()の安全性を高めたsafe_strcat()の実装を紹介します。
↓がコードです。

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

/**
 * 安全性を高めたstrcat
 * ライセンスはMIT
 */
char *safe_strcat(char *dst, size_t dstsz, const char *str) {
    if (!dst || !dstsz || !str) {
        return NULL;
    }

    size_t len = strlen(dst);
    char *p = dst + len;
    char *end = dst + dstsz - 1;

    for (; p < end && *str; ) {
        *p++ = *str++;
    }

    *p = '\0';

    return dst;
}

int main(void) {
    // 以下、safe_strcat()のテストケース
    char s1[100] = "Hello, ";
    assert(safe_strcat(s1, sizeof s1, "World!") == s1);
    assert(!strcmp(s1, "Hello, World!"));

    char s2[2] = "H";
    assert(safe_strcat(s2, sizeof s2, "World!") == s2);
    assert(!strcmp(s2, "H"));    

    char s3[10] = "Hello, ";
    assert(safe_strcat(s3, sizeof s3, "World!") == s3);
    assert(!strcmp(s3, "Hello, Wo"));    

    assert(safe_strcat(NULL, 100, "World!") == NULL);
    assert(safe_strcat("Hello", 0, "World!") == NULL);
    assert(safe_strcat("Hello", 100, NULL) == NULL);
    assert(safe_strcat(NULL, 0, NULL) == NULL);

    return 0;
}

safe_strcat()は第2引数に第1引数のバッファのサイズを取ります。
これを使って内部でバッファーオーバーランにならないようにしています。

ただしこのsafe_strcat()も万能ではありません。
第1引数と第3引数の文字列が同じメモリだった場合の動作は未定義です。
それから第1引数のバッファにナル文字が存在しない場合はstrlen()バッファーオーバーランになる場合があります。
また第1引数に文字列定数を指定した場合もセグフォになる場合があります。

など、色々と問題点は残りますがバッファのサイズを指定できる点ではマシになっていると思います。

このsafe_strcat()を使う場合は自己責任でお願いします。
コードのライセンスはMITです。
何か起こっても責任は取れません。

おわりに

今回はC言語の文字列の連結について詳しく見てみました。
C言語の文字列は原始的な構造ですが、その分高速な処理が可能になっています。
文字列の連結をマスターして皆さんのプログラムで使ってみてください。

文字列に連結したい