質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

関数

関数(ファンクション・メソッド・サブルーチンとも呼ばれる)は、はプログラムのコードの一部であり、ある特定のタスクを処理するように設計されたものです。

配列

配列は、各データの要素(値または変数)が連続的に並べられたデータ構造です。各配列は添え字(INDEX)で識別されています。

Q&A

7回答

1554閲覧

strncpy() 呼び出し時のコピー先の配列の要素数について

KIYZ

総合スコア17

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

関数

関数(ファンクション・メソッド・サブルーチンとも呼ばれる)は、はプログラムのコードの一部であり、ある特定のタスクを処理するように設計されたものです。

配列

配列は、各データの要素(値または変数)が連続的に並べられたデータ構造です。各配列は添え字(INDEX)で識別されています。

0グッド

0クリップ

投稿2018/07/30 01:55

編集2018/07/30 02:07

今月 C の勉強を始めたばかりの者です。
strncpy()関数について学んでいる時に疑問を抱きました。

strncpy()関数呼び出し時にコピー先として指定する配列として、コピーする文字数が収まらない要素数を持つ配列を指定した時、コピーが成功するという現象が起こりました。

###対象のプログラム

プログラム1

C

1#include <stdio.h> 2#include <string.h> 3 4int main(void) 5{ 6 char str1[1] = "foobar"; // 文字数的に収まらない文字列で初期化を試みる 7 8 printf("str1 => %s\n", str1); 9 10 return 0; 11} 12

プログラム1のコンパイル結果 (警告内容は理解しています。) :

main15.c: In function ‘main’: main15.c:29:20: warning: initializer-string for array of chars is too long char str1[1] = "foobar";

プログラム2

C

1#include <stdio.h> 2#include <string.h> 3 4int main(void) 5{ 6 char str1[] = "foobar"; // 配列の要素数の指定は省略。6文字の文字列で初期化。 7 char str2[1]; // 配列の要素数として敢えて "foobar" が収まらない数を指定 8 9 strncpy(str2, str1, 6); // "foobar" の全6文字をコピー 10 str2[6] = '\0'; // コピーした最後の文字の次の位置に終端文字を代入 11 12 printf("str1 => %s\n", str1); 13 printf("str2 => %s\n", str2); 14 15 return 0; 16}

プログラム2の実行結果:

str1 => oobar str2 => foobar

実行環境:
Ubuntu 14.04.5 LTS (Cloud9)
gcc version 8.1.0

参考にした書籍:
苦しんで覚えるC言語 - 文字列処理関数

###疑問

  1. プログラム2のstrncpy()関数でコピー先として指定している配列の要素数は 1 であるため、 "foobar" が収まらないはずですが、実行時にプログラム1のような警告が出ず、正常にコピーが実行されるのはなぜなのでしょうか。
  2. プログラム2のstr2[]宣言時に指定する要素数として、コピーする文字数 + 終端文字 (今回の場合は 7 )未満の数を指定すると、 printf("str1 => %s\n", str1); の結果が "foobar" の一部しか表示されなかったり、何も表示されないという現象が起こりますが、これはなぜなのでしょうか。

なぜコピー元の配列の値が変化することがあるのでしょうか。

他にもこれらの現象を理解する上で知っておくべき概念等がありましたらご教授頂けると幸いです。

よろしくお願い申し上げます。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答7

0

他にもこれらの現象を理解する上で知っておくべき概念等がありましたらご教授頂けると幸いです。

このような事象を、バッファオーバーランといいます。

C言語では配列の範囲チェックが行われないため、はみ出した場所にも書き込めてしまうのですが、最悪の場合、関数から戻る先のアドレスを書き換えることで、不正な動作を実行させることができる脆弱性にもなりえます。

投稿2018/07/30 02:03

maisumakun

総合スコア145184

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

KIYZ

2018/07/30 22:52

>このような事象を、バッファオーバーランといいます。 調べるきっかけとなりました。 セキュリティにまで関わるとは想像できていませんでした。大変勉強になります。 ご回答誠にありがとうございます。
guest

0

範囲チェックが行われないのはすでに y_waiwai さんと maisumakun さんが答えておられますが、これとは別に、環境によってはメモリ確保のときに指定したサイズきっちりしか取らないわけではない、という事象があります。
メモリ管理の都合上で、指定したサイズより大きいサイズで確保されることがあるのです。
※16byte 単位だったりとか、どういう単位の倍数になるかは環境依存です

こうなると、本来ならアクセス違反になるはずが通る、ということにもなったりします。

むろん、そういう動きを前提としてプログラムを組んではいけませんが。

投稿2018/07/30 02:26

tacsheaven

総合スコア13703

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

KIYZ

2018/07/30 23:14

ご回答誠にありがとうございます。 質問のプログラムのような「不具合が生じていることが明らかな事象」より厄介そうですね。 正当な手法で意図通り動作をさせるコードを書かなければ、すぐには不具合が出なくても(気付かなくても)後々大きなバグやセキュリティーホールになり得るというイメージでしょうか。
guest

0

  1. プログラム1の警告はコンパイル時の警告で、これは = で初期化しているためコンパイラによる領域サイズチェックが可能になっています。お使いのコンパイラはstrncpyの仕様(第3引数の数値バイト数だけ第2引数に書き込むことがある)までは知らずにコンパイルするので、コンパイル時エラーは出ません(出せません)。Cでは実行時チェックがない(ようなコードを出力するコンパイラが多い)ため、実行時にもエラーは出ません。で、"foobar" が収まらないのは事実ですが、あふれた先は空き領域だったり他の変数だったりなので、とりあえず読みだしてみたら格納はされているように見えることもあります。
  2.  お使いのコンパイラではstr1とstr2を連続してメモリ上に割り当てているものと思われます。str2にstr1をコピーする過程でオーバーフローした分がstr1を壊しているのでしょう。

 たとえばstr2[1]と宣言した場合変数の初期化が終わった時点ではメモリはこうなっていると思われます(挙動からすると)。

str2[0] 初期化されず
str1[0] 'f'
str1[1] 'o'
str1[2] 'o'
str1[3] 'b'
str1[4] 'a'
str1[5] 'r'
str1[6] '\0'

ここでstrncpyを行うと

str2[0] 'f'
str1[0] 'o'
str1[1] 'o'
str1[2] 'b'
str1[3] 'a'
str1[4] 'r'
str1[5] '\0'
str1[6] '\0'
になります。なので、
printf(str2)するとfoobarが出るし、printf(str1)だとoobarが出る。

のではないかなぁ。

投稿2018/07/30 03:37

a_saitoh

総合スコア702

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

rubato6809

2018/07/30 09:35 編集

ご明察 > a_saitohさん 手元のgccも str2, str1 という順序で配置してます。 配列(一般的に言えば変数)がどんなアドレスに割り当てられたか、質問者は実際に確認してみると良いです。この場合はこれだけ追加すればOK。 printf("str1 = %p\n", str1); printf("str2 = %p\n", str2); おまけ: str2[6] = '\0';  の行を削除(コメントアウト)したらどうなるか、確認するのも軽くお勧め。 C言語では特に、実際のメモリの姿をイメージできないと、不可解な現象に悩むことになります。アドレスを確認するのもそのため。さらにメモリダンプしてみるとか、コンパイラが生成したアセンブリコードに慣れるとか。苦しむ覚悟があるなら、そうした手段を探しておくとよいです。
KIYZ

2018/08/03 08:01

@a_saitoh さん ご丁寧に解説して頂き誠にありがとうございます。 >= で初期化しているためコンパイラによる領域サイズチェックが可能になっています。 初期化していない変数や配列が存在する場合の警告に関する GCC の仕様が気になったので調べてみたところ、 コンパイル時に -O2 オプションを付けるとオーバーフローに関する警告を出せることを知りました。 In file included from /usr/include/string.h:640, from main15.c:2: In function ‘strncpy’, inlined from ‘main’ at main15.c:43:5: /usr/include/x86_64-linux-gnu/bits/string3.h:120:10: warning: ‘__builtin___strncpy_chk’ writing 6 bytes into a region of size 1 overflows the destination [-Wstringop-overflow=] return __builtin___strncpy_chk (__dest, __src, __len, __bos (__dest)); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ >str1とstr2を連続してメモリ上に割り当てている str1 と str2 に割り当てられたメモリアドレスを確認してみたところ、仰る通りになっていました。 str2 に収まりきらなかった分が str1 に割り当てられたメモリに書き込まれてしまう理屈は理解できたのですが、 プログラム2の printf(str2) で str2 を参照した時の結果が (唯一 str2 配列に収まった) f の一文字にならず、 str1 に割り当てられたメモリも参照しているのはなぜなのでしょうか? printf() の仕様を確認したのですが分かりませんでした。 お返事を頂けますと大変助かります。よろしくお願いします。
KIYZ

2018/08/03 08:03

@rubato6809 さん >配列(一般的に言えば変数)がどんなアドレスに割り当てられたか、質問者は実際に確認してみると良い 方法まで教えて頂きありがとうございます。 試してみたところ、 str1 と str2 に割り当てられたアドレスが連続していることが分かり、オーバーフローのイメージを少し掴むことができました。 >str2[6] = '\0';  の行を削除(コメントアウト)したらどうなるか 試してみたところ、 str1 => oobarr str2 => foobarr となったのですが、この結果に至った経緯や理屈はどうしても分かりませんでした。 配列に文字列を代入する時、最後の文字の次に終端文字を代入しなかった場合は最後の文字の次のどこかで偶然 0 が見つかるまでの間に存在する全ての文字が文字列として扱われると学んだのですが、 "foobar" の次に 一文字だけ "r" が入ったのは偶然なのでしょうか。 また、このような未定義動作の挙動の理由を追い求めることも C を学ぶ上で重要なことなのでしょうか? >メモリダンプしてみるとか、コンパイラが生成したアセンブリコードに慣れるとか。苦しむ覚悟があるなら、そうした手段を探しておくとよいです。 このようなアドバイスはとても有り難いです。ありがとうございます。
rubato6809

2018/08/03 13:39

> この結果に至った経緯や理屈はどうしても分かりませんでした そうですか、わかってしまえば簡単なことなんですけど。この現象の裏付けのひとつになると思って「軽く」お勧めしたわけですが、、、 諦めていないなら、私のお勧めは2つ。 1. a_saitohさんが回答の中に描かれたメモリ配置の図をよく見直すこと 2. strncpy() 関数を、KIYZさんご自身で書いてみること 特に2つめ。文字列操作関数群は簡単に書ける関数がほとんどです。strncpy() も簡単な関数です。自らコードを考え、動作をトレースしてみれば、解決するのではないでしょうか。 > 0 が見つかるまで…全ての文字が文字列として扱われると学んだ はい、それが重要な原理です。 > "foobar" の次に 一文字だけ "r" が入ったのは偶然なのでしょうか 「str1 と str2 に割り当てられたアドレスが連続している」状況では必然です。 なお、「一文字だけ "r" が入った」のではなく、最初から存在した "r" が消えずに残ったのです。なぜなら(必ず付けるべきだった)EOSを付けなかったから。 > 未定義動作の挙動の理由を追い求めることも C を学ぶ上で重要? さあ、それは貴方自身がC言語とどう向き合うか、それ次第ではないでしょうか。 未定義動作になるコードを書かない限り、不可解な現象には遭遇しないはすですから。 ただ経験上、こうした挙動と無縁な人は滅多にいません。事実、今回 str2[1] に文字列をコピーするというイリーガルな事を試したのも、「未定義動作の挙動の理由を追い求め」ようとここで質問したのも、貴方自身です。 思いがけずこうした現象をデバッグするであろうことを思えば、ある程度は心得ていたほうが安心ですが、どうせ処理系依存の未定義動作なのだし、デバッグして体得すれば済むことで、今から特別なことをする必要は無い、即ち重要ではないとも言えます。
rubato6809

2018/08/03 22:11 編集

> こうした挙動と無縁な人は滅多にいません 言葉足らずだったので、補足します。 圧倒的に多いケースは、KIYZさんのように意図的に未定義動作をひきおこすのではなく、プログラマは意図と違って未定義な動作を書いてしまい、不可解な現象に「何故?」と悩むのです。次の段落で「思いがけず〜」と続けた意味はそこにあります。 そのようなコードを書く原因は、理解不足だったり、想定外の入力があったり等いろいろありますが、変数や配列のサイズや配置などメモリ上のイメージが明確でないことも大きな要因だと見ています。
a_saitoh

2018/08/06 07:37

Cの「文字列」は他の言語の「文字列型」とはだいぶ異なります。strcpyもそうですが、文字列の先頭の文字へのポインタを渡すのみです。渡された側は、果たしてそのポインタの指している側が何なのかわかりません。文字配列だとしてそのサイズもわかりませんし、もしかしてただの文字変数へのポインタかもしれません。どのcharが文字列配列の最後のバイトかどうかを判断する手段はありません。そういう状況で文字列処理関数は動きます。なので、「渡されたポインタから’¥0’が来るまで」が一つの文字列だとして扱います。'\0'で終端されていない文字列を渡してしまうと、たまたまその続きに配列された変数も文字列の続きとして扱います。続きにあるのがintやdoubleであっても、です。strcpy側にはわからないので。
guest

0

C言語の場合、配列の範囲チェックは行われません。
ですんで、コピー先に収まろうが、はみ出そうが、その関数は実行されます。
その結果、正常に実行終了することもあるでしょうけど、大抵はなにかおかしなことになります

ということで、配列になにかをコピーする場合は、
絶対にコピー先からはみ出さないように、プログラマがきちんとコードを組む必要があります

投稿2018/07/30 02:00

y_waiwai

総合スコア87774

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

KIYZ

2018/07/30 22:46

簡潔で大変分かりやすいご回答をして頂きありがとうございます。
guest

0

strncpy()について、どのように学んだのでしょう?
例えば
MSDNの記述
では

strncpy は、strDest に十分な領域があるかどうかを確認しません。

と書いてあります。
思い込みでプログラムを作るのではなく、最低一度は仕様を確認しましょう。

質問 2. については、
str1とstr2の定義順を入れ替えてやってみてください。
または、str1だけ関数の外で定義するなど、どうなるか見てみてください。

投稿2018/07/30 02:29

nob.

総合スコア711

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

KIYZ

2018/07/30 22:32

ご回答誠にありがとうございます。 Web版の入門書 (https://9cguide.appspot.com/14-03.html#S2) と仕様を説明しているサイトの記述を読んだ後、自分の環境で色々と試していました。 MSDNのページはご回答を頂いてから初めて見ましたが、他では記述していない細かな解説も載っていて良さそうですね。今後は必ず読むようにします。紹介して頂きありがとうございます。 >str1だけ関数の外で定義するなど、どうなるか見てみてください。 実行結果は下記のようになり、 str1 => foobar str2 => foobar さらにメモリアドレスも確認してみると、今までは並んでいたアドレスが離れたアドレスにりました。 str1 address => 0x601048 str2 address => 0x7ffc0b74d93f (私の環境で)同じ関数内かつ連続的に配列を宣言した場合は連続したメモリアドレスが割り当てられるため、 str2 に str1 をコピーする過程でオーバーフローした場合はその分が str1 を壊すことになるが、 str1 と str2 のメモリアドレスが離れている場合は例えオーバーフローしたとしても、 str1 が壊されることにはならない。という解釈で良いのでしょうか。 ( str1 の値が6桁程度の文字列ではなく、膨大なデータだった場合はメモリアドレスが離れていても str1 が壊される可能性があるのでしょうか。)
nob.

2018/07/31 01:25

他の回答者さんが仰っているように、「何が起こるか分からない」と言うことです。 大まかにはKIYZさんがこのコメントでかかれているような理解でいいとは思いますが、必ずそうなる訳でもありません。 デバッグ中におかしな現象が起きて、頭を悩ませることがあった場合、原因としてこの様な「オーバーフロー」を疑うこともあります。 そういう意味で、オーバーフローしたら、どのようなことが起こる可能性があるかを知っておくことは役に立ちます。
guest

0

若干自己満足気味な回答ですが、メモリ内でどんな挙動をしているのか、解説します。

関数内で定義した変数はスタック領域に確保されますが、定義順にスタック領域の最後尾から割り当てられます。
char str1[] = "foobar";で下記のように確保され、

str1[0]str[1]str[2]str[3]str[4]str[5]str[6]
foobar\0

char str2[1];の段階で以下のようになります。

str2[0]str1[0]str[1]str[2]str[3]str[4]str[5]str[6]
(未定義)foobar\0

str2[1]がstr1[0]と同じアドレスになります。

次にstrncpyでstr2へstr1の内容をコピーするのですが、先頭から1文字ずつコピーするので、1文字ずつ表にするとこうなります。

str1[0]をstr2[0]へコピー

str2[0]str1[0]str[1]str[2]str[3]str[4]str[5]str[6]
ffoobar\0

str1[1]をstr2[1]へコピーするが、str2[1]はstr1[0]を同じアドレスになる。

str2[0]str1[0]str[1]str[2]str[3]str[4]str[5]str[6]
fooobar\0

以下同じ

str2[0]str1[0]str[1]str[2]str[3]str[4]str[5]str[6]
fooobar\0
str2[0]str1[0]str[1]str[2]str[3]str[4]str[5]str[6]
foobbar\0
str2[0]str1[0]str[1]str[2]str[3]str[4]str[5]str[6]
foobaar\0
str2[0]str1[0]str[1]str[2]str[3]str[4]str[5]str[6]
foobar\0\0

この結果、str2の先頭アドレスから文字列として見るとfoobar\0となりますがstr1の先頭アドレスから見るとoobar\0となるわけです。

投稿2018/08/03 14:16

hope_mucci

総合スコア4447

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

rubato6809

2018/08/04 05:49 編集

惜しい! 最後のひとつ手前で一段階省いたのが惜しいです。
guest

0

皆さんの回答のように、オーバーランで、一般には、不正な動作です。
ただ、

C

1 char str1[6]; 2 char str2[1]; 3 char str3[100];

というコードで、 str3 が未使用の場合、動く事が多いです。
コンパイラにもよりますが、 str2 へ書込みは、境界を越えて、str3 に書き込まれます。また、str3が無くても、この関数内でのみの場合、同様に動く事が多いです。
当然、-- 運良く -- です。
過去に出会っていますが、触れなかった。(実績のあるコードで、担当でもない -;;)
機会があれば、と思いましたが、そのまま、縁が切れました。

メンテする人が困るコードなので、たとえ、動いても使わないで欲しい。
(バグの発見が困難)

投稿2018/07/30 12:21

pepperleaf

総合スコア6383

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問