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

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

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

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

Q&A

解決済

3回答

4457閲覧

C言語: 変数がスタック領域に積まれる時の動作

REIK727

総合スコア23

C

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

1グッド

0クリップ

投稿2019/04/24 03:12

編集2019/04/24 09:36

C言語で作ったプログラムに於けるスタック領域の仕組みについて質問があります。
まず自分はスタック領域について以下のように理解しています(間違っているかもしれませんが)。

「スタック」とは、後入れ先出しのデータ構造である。

そしてそれを応用したメモリ領域が「スタック領域」である。
スタック領域には変数が宣言された順に積まれていき、変数がいらなくなったタイミングで上から順に削除されていく。
これにより、C言語の「スコープを抜けたら変数は破棄される」という仕様を実現でき、かつメモリの節約にもなる。

この認識が正しいかどうかを確かめるため、以下のようなコードを書いてみました。

C

1#include<stdio.h> 2int func() 3{ 4 int d,e,f; 5 printf("%p,%p,%p\n",&d,&e,&f); 6} 7int main() 8{ 9 int a,b,c; 10 printf("%p,%p,%p\n",&a,&b,&c); 11 func(); 12}

この実行結果が以下の通りです。

0x7ffff9c51f2c,0x7ffff9c51f28,0x7ffff9c51f24

0x7ffff9c51f0c,0x7ffff9c51f08,0x7ffff9c51f04

予想通り変数a,b,cは連続した領域に並んでおり、またd,e,fも連続しています。しかしcとdの間に謎のギャップが存在します。そこで以下のようなコードを書いてみました。

C

1#include<stdio.h> 2int *p1,*p2; 3void func() 4{ 5 int d,e,f;p2=&d; 6 printf("%p,%p,%p\n",&d,&e,&f); 7} 8int main() 9{ 10 int a,b,c;p1=&c; 11 printf("%p,%p,%p\n",&a,&b,&c); 12 func(); 13 printf("%d\n",sizeof(int)*(p1-p2)); 14}

このプログラムを何回か実行した結果が以下の通りです。

0x7fffd5b3aa4c,0x7fffd5b3aa48,0x7fffd5b3aa44

0x7fffd5b3aa2c,0x7fffd5b3aa28,0x7fffd5b3aa24
24

0x7fffdfa7338c,0x7fffdfa73388,0x7fffdfa73384

0x7fffdfa7336c,0x7fffdfa73368,0x7fffdfa73364
24

0x7fffc350ecec,0x7fffc350ece8,0x7fffc350ece4

0x7fffc350eccc,0x7fffc350ecc8,0x7fffc350ecc4
24
...

int型は(私の環境だと)4バイトの大きさを持つので、スタック上で変数cの領域が終わってから変数dの領域が始まるまでに20バイトの空白?があるようです。これはどういった意味を持つのでしょうか?

環境はWindows10 home(64bit)のWSL(ubuntu)、コンパイラはgcc8.3.0でオプションは特につけてないです。

追記

(あっているかは置いといて)結論が出たので勉強の意味も込めて書いておきます。
Zuishin様やcateye様がアドバイスしてくださったように一つ目のコードのアセンブルソースを確認(gcc -S -O0 -fno-asynchronous-unwind-tables <ソース名>)したところ、このようになっていました。

Assembly

1 .file "1.c" 2 .text 3 .section .rodata 4.LC0: 5 .string "%p,%p,%p\n" 6 .text 7 .globl func 8 .type func, @function 9func: 10 pushq %rbp 11 movq %rsp, %rbp 12 subq $16, %rsp 13 leaq -12(%rbp), %rcx 14 leaq -8(%rbp), %rdx 15 leaq -4(%rbp), %rax 16 movq %rax, %rsi 17 movl $.LC0, %edi 18 movl $0, %eax 19 call printf 20 nop 21 leave 22 ret 23 .size func, .-func 24 .globl main 25 .type main, @function 26main: 27 pushq %rbp 28 movq %rsp, %rbp 29 subq $16, %rsp 30 leaq -12(%rbp), %rcx 31 leaq -8(%rbp), %rdx 32 leaq -4(%rbp), %rax 33 movq %rax, %rsi 34 movl $.LC0, %edi 35 movl $0, %eax 36 call printf 37 movl $0, %eax 38 call func 39 movl $0, %eax 40 leave 41 ret 42 .size main, .-main 43 .ident "GCC: (GNU) 8.3.0" 44 .section .note.GNU-stack,"",@progbits

また、Chironian様のご指摘のようにやはりgccはスタックフレームの開始アドレスを16バイト単位に調整しているようです。上のアセンブリソースにsubq $16, %rspという命令があることからもそれが窺えますし、main関数内で宣言する変数をもっと増やしてみたらsubq $32, %rspと変化したことから間違いないかと思います。
最終的に、スタックは恐らくこんな感じになっているのではないかと考えました。(なぜか1列のテーブルが作れなかったのでしかたなく2列にしています)

パディング(16-12=4バイト)
変数f(4バイト)
変数e(4バイト)
変数d(4バイト)
func関数のベースポインタ(8バイト)
main関数への戻り番地(たぶん8バイト?)
パディング(16-12=4バイト)
変数c(4バイト)
変数b(4バイト)
変数a(4バイト)
main関数のベースポインタ(8バイト)

これが変数cと変数dの間の20バイトのギャップの正体ではないかなあと思いました。

dodox86👍を押しています

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

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

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

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

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

guest

回答3

0

ベストアンサー

こんにちは。

スタックには変数だけでなく、実引数、戻り値、CPU内部レジスタの退避、戻り番地も積まれます。
func()には引数がないので実引数は積まれません。戻り値がint型の場合はCPU内部レジスタで返す場合もあります。その場合はスタックには積まれません。CPU内部レジスタの退避や戻り番地は積まれます。
また、gccの場合、スタックフレームの開始アドレスを16バイト単位に調整しているようです。そのためのパディングも含まれているだろうと思います。

投稿2019/04/24 03:55

Chironian

総合スコア23272

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

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

REIK727

2019/04/24 08:57

ご回答ありがとうございます。 詳しく説明していただき、とても参考になりました。スタックには変数だけが積まれる、というのが早とちりだったのですね。 よろしければ質問本文の「追記」部分が正しいか確認していただけると幸いです。
Chironian

2019/04/24 09:21

64bitsビルドですね。であれば合っていると思います。 gccのアセンブル出力を真面目に見たことはないのですが、辻褄は合っています。 (gccは関数にてレジスタを破壊するタイプのようですね。少なくとも昔はレジスタを保存するコンパイラもありました。その場合はレジスタの退避/回復コードが追加されます。)
REIK727

2019/04/24 09:37

どうやらあってるみたいですね。 重ね重ねありがとうございました。
guest

0

標準サブルーチンプロローグ/エピローグ のあたりを見てみたらどうでしょうか?

投稿2019/04/24 03:28

Zuishin

総合スコア28656

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

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

Zuishin

2019/04/24 03:29

逆アセンブルして確認してみるのも良いかもしれません。
REIK727

2019/04/24 08:57

ご回答ありがとうございます。 アセンブリはほんの少ししか触ったことがなく、プロローグ/エピローグという概念も初めて知り、勉強になりました。 よろしければ質問本文の「追記」部分が正しいか確認していただけると幸いです。
guest

0

まず考えられるのは、復帰先アドレスに8バイト(64ビット環境)と関数内利用のレジスタ(bp?など)の退避域+パディングでしょうね?
「追記」わたしも最近のコンパイラの吐くアセンブラには疎いのですが概ね合っているように思います。
こちらでコンパイルしたアセンブラソース(最適化なし,func()のみ)です。サイズなどは合っているようですが、微妙に違う(たぶんGCCのバージョンによるものと思います:gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0)

asm

1func: 2.LFB0: 3 .cfi_startproc 4 pushq %rbp ←ベースポンタ退避 5 .cfi_def_cfa_offset 16 6 .cfi_offset 6, -16 7 movq %rsp, %rbp 8 .cfi_def_cfa_register 6 9 subq $32, %rsp 10 movq %fs:40, %rax 11 movq %rax, -8(%rbp) 12 xorl %eax, %eax 13 leaq -20(%rbp), %rax 14 movq %rax, p2(%rip) 15 leaq -12(%rbp), %rcx ←a 16 leaq -16(%rbp), %rdx ←b 17 leaq -20(%rbp), %rax ←c 18 movq %rax, %rsi 19 leaq .LC0(%rip), %rdi ←printfに渡す文字列のアドレス(たぶんw) 20 movl $0, %eax 21 call printf@PLT 22 nop 23 movq -8(%rbp), %rax 24 xorq %fs:40, %rax 25 je .L2 26 call __stack_chk_fail@PLT 27.L2: 28 leave 29 .cfi_def_cfa 7, 8 30 ret 31 .cfi_endproc

最適化: -pipe -std=c11 -Wall -Ofast ↓

asm

1func: 2.LFB12: 3 .cfi_startproc 4 subq $40, %rsp 5 .cfi_def_cfa_offset 48 6 leaq .LC0(%rip), %rsi 7 movl $1, %edi 8 movq %fs:40, %rax 9 movq %rax, 24(%rsp) 10 xorl %eax, %eax 11 leaq 12(%rsp), %rdx 12 leaq 16(%rsp), %rcx 13 leaq 20(%rsp), %r8 14 movq %rdx, p2(%rip) 15 call __printf_chk@PLT 16 movq 24(%rsp), %rax 17 xorq %fs:40, %rax 18 jne .L5 19 addq $40, %rsp 20 .cfi_remember_state 21 .cfi_def_cfa_offset 8 22 ret

同じソースを最新clang(clang version 9.0.0 (trunk 358576))に食わせてみました・・・みじかい@@;
オプション: -pipe -std=c11 -Wno-padded -Weverything -Ofast

asm

1func: # @func 2 .cfi_startproc 3# %bb.0: 4 subq $24, %rsp 5 .cfi_def_cfa_offset 32 6 leaq 20(%rsp), %rsi 7 movq %rsi, p2(%rip) 8 leaq 16(%rsp), %rdx 9 leaq 12(%rsp), %rcx 10 movl $.L.str, %edi 11 xorl %eax, %eax 12 callq printf 13 addq $24, %rsp 14 .cfi_def_cfa_offset 8 15 retq

投稿2019/04/24 03:40

編集2019/04/24 09:57
cateye

総合スコア6851

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

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

cateye

2019/04/24 03:50

Zuishinさんの言うように処理系依存な部分もあるので、コンパイル時に-Sオプションをつけてアセンブルソースを見てみるのが一番かと・・・
REIK727

2019/04/24 08:57

ご回答ありがとうございます。 復帰先に退避域、パディングですね。恥ずかしながらアセンブリはほとんど経験がないので勉強になりました。 よろしければ質問本文の「追記」部分が正しいか確認していただけると幸いです。
cateye

2019/04/24 09:37 編集

最適化オプションしてありますか・・・スタック(sp)の下がり方がちょっと気になったので? subq $32, %rspとsubq $16, %rsp・・・??
REIK727

2019/04/24 09:39

すみません、アセンブリソースを出力させるときのオプションが明記されていませんでした。 質問本文にも追記しましたが、gcc -S -O0 -fno-asynchronous-unwind-tables <ソース名>という感じで動かしました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.50%

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

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

質問する

関連した質問