5

これはSIGSEGVを出しませんが、なぜですか?

package main

import "fmt"

func get_pointer() *int{
    var x int = 1
    fmt.Println(&x)
    return &x
}

func main() {
    xp := get_pointer()
    *xp = 100
    fmt.Println(xp)
    fmt.Println(*xp)
}
0xc00002c008
0xc00002c008
100
cubick
  • 20,987
  • 5
  • 25
  • 64
misaki
  • 680
  • 6
  • 14

3 Answers3

6

変数 x がスタックではなくヒープ領域に確保されたためです。

$ go run -gcflags '-m' main.go
# command-line-arguments
./main.go:7:13: inlining call to fmt.Println
./main.go:14:13: inlining call to fmt.Println
./main.go:15:13: inlining call to fmt.Println
=> ./main.go:6:6: moved to heap: x <=
./main.go:7:13: []interface {} literal does not escape
./main.go:14:13: []interface {} literal does not escape
./main.go:15:14: *xp escapes to heap
./main.go:15:13: []interface {} literal does not escape
<autogenerated>:1: .this does not escape
0xc000014178
0xc000014178
100

これは golang の escape analysis と呼ばれる機能です。興味を持たれましたら Allocation efficiency in high-performance Go services の "Some Pointers" を参照してみて下さい。

4

Go ではポインタによってデータへの参照が関数スコープ外に漏れているかどうかコンパイラが解析しており(エスケープ解析)、これに従ってデータをスタックに置くかヒープに置くか管理しています。このため C などとは違い関数スコープを気にせずポインタを return して良いです。ヒープに置かれ使われなくなったデータは GC によって処理されます。

このことはたとえば Go の FAQ にも以下のように書かれています。

How do I know whether a variable is allocated on the heap or the stack?

From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language. (以下略)

(日本語訳)

変数がヒープにあるかスタックにあるか、どうすれば知れますか?

正確性を期すならば、知る必要はありません。Go ではそれぞれの変数はそこへの参照がある限り存在します。保存場所が実装によってどう選ばれるかは言語の意味論とは関係しません。(以下略)

nekketsuuu
  • 23,683
  • 11
  • 50
  • 115
0

(追記)metropolis さんの回答等に示されているように、このご質問の場合、「変数がスタックではなくヒープ領域に確保されるため、危険な参照とはならない」と言うのが正解と思われます。ただ、「誤ったポインタを操作しても直ちにSIGSEGVになるとは限らない」と言う点は成り立ちますので、とりあえずこの回答はそのまま残しておきます。


残念ながら、誤ったポインターの使い方の全てが直ちにSIGSEGVのようなCPU例外を引き起こすとは限りません。

例のようにローカル変数(Go言語で「自動変数」と言う言い方があるのかどうかは確かめられませんでした)へのポインターを戻してしまうと、そのポインターは既に解放された後のスタック領域を指すわけですが、スタック領域は、あなたのプログラムから書き込み可能となるようメモリが割り当てられており、その領域を読み出したり、書き込んだりしても、メモリが割り当てられている限り、SIGSEGVは発生しません。

     スタック
xp->|      |
    | :    | ↑スタックの伸びる方向
    |------| <- CPUのスタックポインタ、本来これより低位のメモリアドレスにはアクセスしてはいけない 
    |      |
    | :    |

例えば、xpの指すアドレスに対応するメモリが仮想記憶管理の関係で解放されてしまっていたり、xpに書き込んだ時にその領域が別の用途に再利用されている場合などにはSIGSEGVが発生する可能性もあるのですが、そのような条件が成り立たない場合には不幸なことに「見かけ上動いているように見える」ことになります。

これはSIGSEGVを出しませんが、なぜですか?

誤ったポインタの使い方をしても、必ずSIGSEGVが出るとは限りません。自明におかしい場合にはコンパイラが警告などを出すと思いますが、「動かしてみたが問題なく動いているように見える」ことで安心せずに、注意しないといけません。

OOPer
  • 19,157
  • 2
  • 16
  • 33
  • 他の回答に書かれているように、今回の例では変数 x はスタックではなくヒープに置かれます。 – nekketsuuu Apr 29 '20 at 10:24
  • 正直、metropolis さんの書かれたような escape analysis については知りませんでした。「Go言語で」と書いていながら、発想がC言語のままだったようです。ただ、「スタック上のポインタを操作しても直ちにSIGSEGVになるとは限らない」と言うの点は成り立ちますので、とりあえず回答はそのまま残しておきます。 – OOPer Apr 29 '20 at 10:26
  • 1
    Go のコンパイラは外部で参照される可能性があるならばスタック上には置かれません。なので SEGV も起きません。 – mattn May 04 '20 at 01:29
  • @mattn さん、そのご指摘は既にnekketsuuu♦ さんのコメントや、metropolis さんの回答に書かれていることではないかと思います。それ以外の何かについてお知らせいただいているのでしょうか? – OOPer May 04 '20 at 03:04
  • Go においては「「スタック上のポインタを操作しても直ちにSIGSEGVになるとは限らない」と言うの点は成り立ちますので」が成り立たないという意味で書きました。 – mattn May 04 '20 at 16:16
  • @mattn さん、metropolis さんの回答や、nekketsuuu♦ さんの示されたリンクで、『質問のような場合は「スタック上のポインタを操作」することにはならない』と言うのは理解できたのですが、『Go においては「「スタック上のポインタを操作しても直ちにSIGSEGVになるとは限らない」と言うの点は成り立ちますので」が成り立たない』と言うのは理解できません。何らかのリンクで結構ですので、信頼できるソースをお示しいただけますでしょうか? – OOPer May 05 '20 at 04:19
  • Go は明示的に変数をスタックに置く事はできません。スタックに置くかヒープに置くかはコンパイラが決定します。関数から変数のアドレスを戻り値として返して、外部でデリファレンスされる様なコードがあれば、コンパイラは自動的に変数をヒープに置きます。

    https://golang.org/doc/faq#stack_or_heap

    – mattn May 06 '20 at 12:55
  • @mattn さん、そのことについては、metropolis さんの回答や、nekketsuuu♦ さんの示されたリンクで十分承知していますので、改めて別のリンクを示していただく必要はありません。あなたは「「スタック上のポインタを操作しても直ちにSIGSEGVになるとは限らない」と言うの点は成り立ちますので」を否定しているので、「スタック上のポインタを操作すれば、必ず直ちにSIGSEGVになる」と主張されていることになるのですが、ご自身でその点を理解しておられるのでしょうか? – OOPer May 06 '20 at 12:58
  • また Go のランタイムはアドレスのチェックや bounds checker が導入されているので、不正なアドレスを参照すると自動的に panic を起こす様になっています。 – mattn May 06 '20 at 12:59
  • @mattn さん、OSの機能を直接いじるわけではない「アドレスのチェックや bounds checker 」で、一体どの程度の不正アクセスが検出できるのか、と言うのは十分ご存知でしょうか? – OOPer May 06 '20 at 13:02
  • Go で扱うポインタは Go で生成した物です。cgo という機能はありますが、C言語のライブラリに対してGoのポインタは渡せなくなっています。またC言語の様にポインタ演算はできません。 – mattn May 06 '20 at 13:06
  • @mattn さん、先ほど書いた『「スタック上のポインタを操作すれば、必ず直ちにSIGSEGVになる」と主張されていることになる』という点について返事をいただいていないのですが、その点についての認識はいかがですか? それともご自身の主張はそういうことではなかったということを認められたと考えていいのでしょうか? – OOPer May 06 '20 at 13:09
  • スタック上のポインタを操作して壊れる様な事をすれば直ちに panic を起こす様になっていると思います。どの様なケースをお考えですか? – mattn May 06 '20 at 13:13
  • @mattn さん、まずあなたは「必ずSIGSEGV(と言うsignal)が発生する」と言うのと、「Go言語のランタイムがpanicを起こす」と言うのが区別できていないようです。またランタイムによる不正アクセス検出が一体どれだけの検出能力を持つのかについても十分ご存知ないようです。私としては、あなたが「思います」と言うのではなく、ありとあらゆる不正なポインタアクセスが「SIGSEGVを引き起こす」と言う根拠があればお聞きしたかったのですが、どうやらあなたがそう思っていると言う点以上のことは、お聞きできないようです。スタックオーバーフローのサイトとしてのコメント制限に引っ掛かったようなので、これ以上コメントの形で返信はしませんが、まずはUnix/Linuxでどのような不正メモリアクセスがSIGSEGVになるのかと言う点については十分に勉強し直されると良いと思います。また、metropolis さんやnekketsuuu♦ さんの回答やそこに書かれたリンク先を十分読んでおられたのかも疑問に思います。質問への回答とはなっていない回答を残す理由がわかりやすく示せていないことに関しては、コメントをつけていただいたことには感謝しておりますが、なぜそこまで執拗にmetropolis さんやnekketsuuu♦ さんに示していただいたことの繰り返しになってることを認めたがらないのかは理解不能です – OOPer May 06 '20 at 13:27
  • 前述の様に Go はC言語の様にポインタに対する加算や減算ができない言語ですのでおっしゃっておられる SEGV を起こせないのです。この回答はC言語の様にポインタの加算や減算があるプログラミング言語にとっては有用ですがGoに関してはあまり的を得ていないと思いコメントさせて頂きました。 – mattn May 06 '20 at 13:39