スポンサーリンク

このドキュメントの内容は、以下の通りです。

はじめに

パソコンやスマホを利用していて、使用中のアプリが突然落ちたり、画面が真っ黒になったと思ったらオペレーティングシステムが落ちてしまった、という経験はないでしょうか?オペレーティングシステムやアプリケーションなどのソフトウェアは、ソフトウェアのコーディング中にちょっとした間違えをしてしまうと、簡単に落ちるソフトウェアになってしまいます。

[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では、 libcstrlenの中で何がおきているのかよくわかりませんでした。そこで、 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]

スポンサーリンク
スポンサーリンク
 
いつもシェア、ありがとうございます!


もっと情報を探しませんか?

関連記事

最近の記事

人気のページ

スポンサーリンク
 

過去ログ

2020 : 01 02 03 04 05 06 07 08 09 10 11 12
2019 : 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

サイト

Vim入門

C言語入門

C++入門

JavaScript/Node.js入門

Python入門

FreeBSD入門

Ubuntu入門

セキュリティ入門

パソコン自作入門

ブログ

トップ


プライバシーポリシー