C言語 strlenにNULLを突っ込むとセグメンテーションフォルト
スポンサーリンク
このドキュメントの内容は、以下の通りです。
- はじめに
- セグメンテーションフォルトを起こすコード
- サンプルコード strlen.c
- コンパイル方法
- 実行例
- デバッガで様子を観察する
- strlenの実装を取り込んでみる
- どのように対処すれば良いのか?
はじめに
パソコンやスマホを利用していて、使用中のアプリが突然落ちたり、画面が真っ黒になったと思ったらオペレーティングシステムが落ちてしまった、という経験はないでしょうか?オペレーティングシステムやアプリケーションなどのソフトウェアは、ソフトウェアのコーディング中にちょっとした間違えをしてしまうと、簡単に落ちるソフトウェアになってしまいます。[2011-12-09-1] の C++言語の string 型に NULLを突っ込むとセグメンテーションフォルトになるという話と似た話です。
セグメンテーションフォルトを起こすコード
C言語の以下のコードを書いたときに、 char型のポインタの *p にアクセスすることができないので、セグメンテーションフォルト(Segmentation Fault)を引き起こします。char *p = NULL;
そもそも、このようなコード(strlen.c)は、書いたりしないので、 ありえないはず、なんですが。
ポインタを受け取ったら、チェックするでしょ?チェックをします。チェックをしないといけないのです。さもなければ、セグっちゃうよ(Segmentation Fault)、ということであります。
[2011-12-09-1] の 「std::string に NULL を突っ込むとセグメンテーションフォルト」で std::string に NULL をつっこんだわけですが、FreeBSD の libstdc++.so の場合、 std::string が libc の strlen () を呼び出しているらしく、そういう意味で、今回の話につながってると思います。
サンプルコード strlen.c
以下は、C言語のサンプルコードです。 charのポインタに NULL を代入します。 NULL のポインタを strlen に渡すだけの簡単なプログラムです。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int
main (int argc, char *argv[])
{
int i = 0;
char *p = NULL;
i = strlen (p);
exit (EXIT_SUCCESS);
}
コンパイル方法
コンパイル方法は、以下の通りです。gcc -g strlen.c
gcc に -g オプションをつけているのは、あとで、デバッグするためです。
実行例
サンプルプログラムを実行すると、セグメンテーションフォルトが発生しました。% ./a.out Segmentation fault Exit 139
終了ステータスが 139 となっています。 strlen の関数を呼び出したあとに、その次の exit にたどり着いていない、ということになります。
デバッガで様子を観察する
Unix には GDB と呼ばれる GNU が開発したデバッガがあります。gdb で a.out のプログラムの動きを観察してみましょう。以下は、FreeBSD の環境で gcc / gdb を利用して得られた結果です。 Linux の環境だと、少し違った表示になるかと思います。なお、 FreeBSD 12.1-RELEASE では、 gdb がデフォルトでは入らなくなったようです。 pkg コマンドで別途 gdb をインストールする必要があります。
% gdb a.out (gdb) run Starting program: a.out Program received signal SIGSEGV, Segmentation fault. 0x28174d2d in strlen () from /lib/libc.so.7 (gdb) bt #0 0x28174d2d in strlen () from /lib/libc.so.7 #1 0x08048423 in main () at strlen.c:18
上記の内容を読む限り、 strlen() の中で、プログラムがおかしくなったと読み取れます。
Linux の場合はどうなるのか、と思って、Ubuntu の環境でも確認いたしました。 Ubuntu 18.04.4 LTS の gcc 7.5.0, gdb 8.1.0.20180409-git の場合、以下の通りでした。
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git Copyright (C) 2018 Free Software Foundation, Inc. 省略 This GDB was configured as "x86_64-linux-gnu". 省略 Reading symbols from ./a.out...(no debugging symbols found)...done. (gdb) run Starting program: /tmp/a.out Program received signal SIGSEGV, Segmentation fault. __strlen_sse2 () at ../sysdeps/x86_64/multiarch/../strlen.S:120 120 ../sysdeps/x86_64/multiarch/../strlen.S: そのようなファイルやディレクトリはありません. (gdb)
Linux だと strlen の本体部分は __strlen_sse2 なのでしょうか?
strlenの実装を取り込んでみる
gdbでは、 libcの strlenの中で何がおきているのかよくわかりませんでした。そこで、 FreeBSD の strlen のソースコードを関数名だけ変更して、テストコードに取り込んで、ビルドしてみました。
下記が gdb でデバッグ実行した結果です。 my_strlen の中身は、 FreeBSD 12.1-RELEASE の strlen そのものです。
Program received signal SIGSEGV, Segmentation fault. 0x0000000000201338 in my_strlen (str=0x0) at strlen.c:101 101 va = (*lp - mask01);
以下は、GDBでステップ実行をした結果です。ここでは my_strlen ということになっていますが FreeBSD の strlenの引数のcharポインタ型の str が lp に代入されていますが、 lp の中身を確認してみると、 lp は 0x0 ですね。 0x0 にアクセスできないので、クラッシュします。
Breakpoint 1, my_strlen (str=0x0) at strlen.c:100 100 lp = (const unsigned long *)((uintptr_t)str & ~LONGPTR_MASK); (gdb) n 101 va = (*lp - mask01); (gdb) p lp $5 = (const unsigned long *) 0x0
この記事の表記上は無関係ですが、GDBはGNU source-highlightのおかげで、ソースコードやデバッグの表示もカラフルです。ターミナルの色設定によっては、逆に読みにくいケースもあるかもしれません。
以下は、実際の FreeBSD の strlen の実装の抜粋です。
strlen()は、32ビットと64ビットシステムのポータブルな実装になっているため、少し難しいコードになっていると思います。 32ビットだけの頃は、もっと簡単な実装だったのではないでしょうか。 ヌル文字(\0)までループを回す、という意味では、変化はないと思います。
/* Magic numbers for the algorithm */
#if LONG_BIT == 32
static const unsigned long mask01 = 0x01010101;
static const unsigned long mask80 = 0x80808080;
#elif LONG_BIT == 64
static const unsigned long mask01 = 0x0101010101010101;
static const unsigned long mask80 = 0x8080808080808080;
#else
#error Unsupported word size
#endif
#define LONGPTR_MASK (sizeof(long) - 1)
/*
* Helper macro to return string length if we caught the zero
* byte.
*/
#define testbyte(x) \
do { \
if (p[x] == '\0') \
return (p - str + x); \
} while (0)
size_t
strlen(const char *str)
{
const char *p;
const unsigned long *lp;
long va, vb;
/*
* Before trying the hard (unaligned byte-by-byte access) way
* to figure out whether there is a nul character, try to see
* if there is a nul character is within this accessible word
* first.
*
* p and (p & ~LONGPTR_MASK) must be equally accessible since
* they always fall in the same memory page, as long as page
* boundaries is integral multiple of word size.
*/
lp = (const unsigned long *)((uintptr_t)str & ~LONGPTR_MASK);
va = (*lp - mask01);
vb = ((~*lp) & mask80);
lp++;
if (va & vb)
/* Check if we have \0 in the first part */
for (p = str; p < (const char *)lp; p++)
if (*p == '\0')
return (p - str);
/* Scan the rest of the string using word sized operation */
for (; ; lp++) {
va = (*lp - mask01);
vb = ((~*lp) & mask80);
if (va & vb) {
p = (const char *)(lp);
testbyte(0);
testbyte(1);
testbyte(2);
testbyte(3);
#if (LONG_BIT >= 64)
testbyte(4);
testbyte(5);
testbyte(6);
testbyte(7);
#endif
}
}
/* NOTREACHED */
return (0);
}
FreeBSD の strlen の実装は、FreeBSDをインストールしたディスクの /usr/src/lib/libc/string/strlen.c にあります。
どのように対処すれば良いのか?
このようなケースにおいて、どのように、コーディングをして対処すればよいでしょうか?一般的には、C言語でポインタを受け取る場合には、そのポインタが有効なポインタであるか、チェックをしなければなりません。有効なポインタというのは、少なくとも、NULLのポインタではない、ということを確認することになります。つまり、サンプルコードでは、変数 p が NULL ではないかを確認しなければなりません。
if (p)
{
i = strlen (p);
} else {
// 何らかのエラー処理が必要であれば、エラー処理を書きましょう
}
もし、p が NULL であれば、 strlen() を実行してはなりません。 p が NULL の場合にどのようにするかは、ソフトウェアの設計によるので、適切なエラー処理などを書かなければなりません。
このように、ちょっと NULL を渡しただけで、ソフトウェアというのは、停止してしまう、非常に繊細なものなのです。
参照しているページ (サイト内): [2011-12-09-1]
スポンサーリンク
スポンサーリンク
いつもシェア、ありがとうございます!
もっと情報を探しませんか?
関連記事
最近の記事
- パナソニック ジェットウォッシャードルツ EW-DJ61-Wのホースの修理
- LinuxセキュリティモジュールIntegrity Policy Enforcement
- アマゾンのEcho Show 5を買ったのでレビューします
- アマゾンのサイバーマンデーはAlexa Echo Show 5が安い
- Android スマートフォン OnePlus 7T と OnePlus 7の違い
- Android スマートフォン OnePlus 7 をAndroid10にアップデートしてみた
- クレジットカードのバーチャルカードの比較のまとめ
- 活動量計 Xiaomi Mi Band 4を買ってみたのでレビュー
- Android スマートフォン OnePlus 7 のレビュー
- AliExpressでスマートフォンを買い物してみた
- パソコンのホコリ対策 レンジフードフィルターと養生テープ
- 80PLUS GOLDのPC電源ユニットAntec NeoEco 750 Goldのレビュー
- イギリスの付加価値税 VAT は払い戻しを受けられる
- イギリスのロンドンでスーツケースなど荷物を預けられる場所は
- イギリスのロンドンで地下鉄やバスに乗るならオイスターカードを使おう
- イギリスのヒースロー空港からロンドン市内への行き方
- 航空便でほかの航空会社に乗り継ぎがある場合のオンラインチェックイン
- SFC会員がANA便ではなくベトナム航空のコードシェアを試して解ったこと
- ベトナムの入国審査でeチケットの掲示が必要だった話
- シアトルの交通ICカードはオルカカード(Orca)です
人気のページ
- Windows7 IME 辞書ツールで単語の登録に失敗しました
- C言語 popen()でコマンドを実行して出力を読み込む
- Windows7で休止状態にする方法
- CentOS MySQLの起動、停止、再起動
- loggerコマンドでsyslogにエラーを出力する方法
- パソコンパーツの買取をしてくれる店のまとめ
- Java Mapの使い方 get(),put(),remove(),size(),clear()
- 楽天のRポイントカードを作ってみた
- iPhone 5 から iPhone 6 に乗り換えたのでレビュー
- netstatコマンドのステータスの意味
スポンサーリンク
過去ログ
2020 : 01 02 03 04 05 06 07 08 09 10 11 122019 : 01 02 03 04 05 06 07 08 09 10 11 12
2018 : 01 02 03 04 05 06 07 08 09 10 11 12
2017 : 01 02 03 04 05 06 07 08 09 10 11 12
2016 : 01 02 03 04 05 06 07 08 09 10 11 12
2015 : 01 02 03 04 05 06 07 08 09 10 11 12
2014 : 01 02 03 04 05 06 07 08 09 10 11 12
2013 : 01 02 03 04 05 06 07 08 09 10 11 12
2012 : 01 02 03 04 05 06 07 08 09 10 11 12
2011 : 01 02 03 04 05 06 07 08 09 10 11 12
2010 : 01 02 03 04 05 06 07 08 09 10 11 12
2009 : 01 02 03 04 05 06 07 08 09 10 11 12
2008 : 01 02 03 04 05 06 07 08 09 10 11 12
2007 : 01 02 03 04 05 06 07 08 09 10 11 12
2006 : 01 02 03 04 05 06 07 08 09 10 11 12
2005 : 01 02 03 04 05 06 07 08 09 10 11 12
2004 : 01 02 03 04 05 06 07 08 09 10 11 12
2003 : 01 02 03 04 05 06 07 08 09 10 11 12