ホーム » スタッフ » 斉藤徹 » 講義録 » 情報構造論 » メモリ管理・スタック領域

メモリ管理・スタック領域

ここまでの授業では、プログラムを動かすうえでアルゴリズムとデータ構造を中心に話をしてきた。しかしプログラムの中で利用しているデータがどういったメモリで管理されているのかを正しく理解する必要がある。そこで、局所変数のようなデータを保存するためのスタック領域と、new 命令で確保されるヒープ領域についてその使われ方などについて理解する。

C言語やJavaでのメモリ領域(静的領域とスタック領域)

C言語では、データ領域は、定数領域、静的変数領域、スタック領域、ヒープ領域 で構成される。また、変数にはスコープという変数が使える範囲がある。

定数領域は、値が変化しないデータが保存される。

静的変数領域は、プログラムが起動したときに確保され、プログラム終了と共にデータが消される。

スタック領域は、関数が呼び出される時に確保され、関数を抜ける時にデータが消される。関数の引数や関数の局所変数などは、この領域に保存される。

#include <stdio.h>

int x = 123 ;            // 静的大域変数
const int y = 234 ;      // 静的大域変数(定数) 再代入不可

void foo() {
    int b = 345 ;        // 動的局所変数
    b++ ;
    printf( "%d %d\n" , x , b ) ;
}

void bar( int a ) {
    int b = 456 ;        // 動的局所変数
    static int c = 789 ; // 静的局所変数
    x++ ; b++ ; c++ ;
    printf( "%d %d %d\n" , x , b , c ) ;
    foo() ;
}

int main() {
    int z = 890 ;        // 動的局所変数
    bar( z ) ;
    bar( z ) ;
    printf( "%d\n" , z ) ;
    return 0 ;
}

// 実行結果
// 124 457 790
// 124 346
// 125 457 791
// 125 346
// 890

大域変数は混乱の元

以下のようなプログラムでは、foo() を実行すると”0 1 2″ と表示され、main の中で foo() を3回呼び出しているので、”012,012,012″と表示されると勘違いするかもしれない。しかし、xが大域変数(Javaでは大域変数は無いけど)であるため、foo() の処理の中で x=3 となっているため、mainの中では、2回目のループが動かないため、”0 1 2″と表示されるだけである。

こういったように、誰もが使える変数を、どこからでも呼び出せる状態にしておくとプログラムの間違いが発生しやすい。

// C言語での大域変数の問題
int x ;
void foo() { // 0 1 2 と出力
   for( x = 0 ; x < 3 ; x++ )
      printf( "%d\n" , x ) ;
}
int main() {  // 0 1 2 を出力する処理を 3回繰り返すと 0 1 2,0 1 2,0 1 2 と出力される?
   for( x = 0 ; x < 3 ; x++ )
      foo() ;
   return 0 ;
}
// Javaでの大域変数の問題
public class Main {
    public static int x = 0 ; // 静的クラス変数
    public static void foo() { // 0 1 2 と出力
        for( x = 0 ; x < 3 ; x++ )
            System.out.println( x ) ;
    }
    public static void main(String[] args) throws Exception {
        for( x = 0 ; x < 3 ; x++ ) // 0 1 2 を出力する処理を 3回繰り返すと 0 1 2,0 1 2,0 1 2 と出力される?
            foo() ;
    }
}

こういう場合は、正しく局所変数を用いて、関数内でのみ使う変数 x を宣言すれば、上記のような間違いを防ぐことができる。関数内で宣言される変数は関数に入る度にメモリを確保し、関数を抜ける時にメモリ領域が消される。

// C言語での大域変数の解決のために局所変数を使う
void foo() { // 0 1 2 と出力
   int x ;
   for( x = 0 ; x < 3 ; x++ )
      printf( "%d\n" , x ) ;
}
int main() {  // 0 1 2 を出力する処理を 3回繰り返す
   int x ;
   for( x = 0 ; x < 3 ; x++ )
      foo() ;
   return 0 ;
}
// Javaでの大域変数の解決のために局所変数を使う
public class Main {
    public static void foo() { // 0 1 2 と出力
        int x ;
        for( x = 0 ; x < 3 ; x++ )
            System.out.println( x ) ;
    }
    public static void main(String[] args) throws Exception {
        int x ;
        for( x = 0 ; x < 3 ; x++ ) // 0 1 2 を出力する処理を 3回繰り返す
            foo() ;
    }
}

一方で、関数が呼び出された回数を確認したい…という用途であれば、下記のように大域変数を使うこともあるが、これだと x を間違って使われる可能性がある。

int x = 0 ;
void foo() {
   x++ ;
   printf( "%d\n" , x ) ;
}
int main() {
   foo() ;
   foo() ;
   return 0 ;
}

このために、C言語では静的局所変数というのがある。関数内で static で宣言された変数は、その関数の中でしか使えないが、プログラムが起動した時に変数領域が作られ初期化され、プログラムが終了した時にデータ領域が消される。

void foo() {
   static int x = 0 ;
   x++ ;
   printf( "%d\n" , x ) ;
}
int main() {
   foo() ;
   foo() ;
}

Javaでは、プログラムの中でデータが間違ってアクセスされることを防ぐために、大域変数という考え方は存在しない。その代わりにクラス内で共通に利用できる変数ということで、静的なクラス変数が用いられる。static なクラス変数は、クラスがロードされた時点でメモリに確保され、プログラム終了まで保持される。

public class MyClass {
    public static int count = 0; // クラス変数

    public static void main(String[] args) {
        MyClass.count++; // インスタンスを作らずにアクセス
        System.out.println(MyClass.count);
    }
}

スタック領域

スタック領域は、ここまでに述べた様に「関数を呼び出す際にメモリ領域が確保・初期化され、関数が終わるとメモリ領域は消される。

以下のような main から bar() , foo() が呼び出される処理では、

  1. 関数呼び出し時には、戻り番地の保存、実引数の確保が行われ、
  2. 関数に入った時点で局所変数の領域が確保される。
  3. 関数が終わると、実引数・局所変数の領域が消され、スタックから取り出された戻り番地に処理を移行する。

このような関数呼び出しでは、最後(Last)に確保した変数が最初(First)に忘れればいいというデータなので、Last In First Out の スタック構造が使われる。

バッファオーバーフロー

C言語でのスタックは、前述のように行われるが、局所変数の覚え方を悪用した攻撃が存在する。

クラッカーがサーバを攻撃する場合、サーバ上のプログラムの脆弱性を利用する。
サーバプログラムの脆弱性を利用する最も典型的な攻撃方法には、バッファオーバーフローがある。


こういった問題が含まれるアプリケーションは危険であり、こういった脆弱性が見つかったらプログラムの更新が重要である。

広く利用されているソフトウェアでも日々脆弱性が見つかる。(JVNなどのサイトでは脆弱性情報を収集・公開してくれている)

Rust とセキュリティ

C言語では、バッファオーバーフローのような方法で脆弱性などが発生することから、メモリの使い方をもっと厳格に管理するプログラム言語として、Rust が注目されている。Rust は、性能、メモリ安全性、安全な並行性を目指して設計されたプログラミング言語である。C言語、C++に代わるシステムプログラミング言語を目指しており、構文的にはC++に似ていて、「ボローチェッカー」(borrow checker) で参照の有効性を検証することによってメモリ安全性を保証できる。(Wikipedia引用)

最近では、Linux の OS で Rust を使う方針や、Microsoft で AI を活用して C,C++をRustに置き換えるなどの話から、注目されている。