ARM アーキテクチャと C 言語 (魏東山) 学習メモ (2) - グローバル変数、ヒープ、スタック


1. グローバル変数が初期化されない理由

#include "stdio.h"
int g_a=123;
int add_val(volatile int v){
    
    
    volatile int a=321;
    v=v+a;
    return v;
}
int main(){
    
    
    static volatile int s_a=1;
    volatile int b;
    b=add_val(s_a);
    return 0;
}

この関数では、前節で述べたように、関数 add_val(volatile int v) 内のローカル変数 a が初期化されてスタック領域に割り当てられますが、なぜグローバル変数 int g_a=123 と static volatile int s_a=1 が定義されているのでしょうか。 ; 初期化するコマンド?

2. グローバル変数の初期化方法

ローカル変数の場合、指定バイトのデータ空間と指定バイトのアドレス空間を割り当てる初期化が行われます。ローカル変数はRAMに格納されます。グローバル変数の場合、グローバル変数はプログラム内のどこからでもアクセスできるため、通常はRAMに格納されます。プログラムが開始されると、グローバル変数が初期化され、メモリ空間が割り当てられます。ただし、グローバル変数を定義するときに const キーワードを使用する場合、またはグローバル変数を静的型として定義する場合、これらの変数は通常、RAM ではなくフラッシュに格納されます。これは、const 型の変数は一般に定数とみなされ、値が変更されないため、Flash に保存できるためです。静的型の変数はプログラム実行中に一度だけ初期化され、プログラム実行中ずっと存在するため、Flashに格納することもできます。
フラッシュ内のグローバル変数の場合、読み取りおよび書き込み時に RAM 内の指定されたアドレスにコピーされます。このアドレスはコンパイラとシステムによって決定されます。
ここに画像の説明を挿入
KEIL では、リンカ LINKER が RAM ベース アドレスと FLASH ベース アドレスを設定します。したがって、CPU はまずフラッシュからデータ アドレスを読み取り、そのデータを R0 レジスタにコピーします。次に、このグローバル変数は RAM のアドレス 0x20000000 に保存されます。

3. スタックとは何ですか

ローカル変数とグローバル変数の研究によると、私はスタックの概念をおそらく知っています。

スタック(stack)は、「後入れ先出し(LIFO)」の原理に従ったコンピュータメモリ内のデータ構造です。スタックでは、データの追加と削除はスタックの最上位でのみ行われるため、スタックに追加された最後のデータが最初に削除されます。
コンピューターでは、** スタックは通常、パラメーター、ローカル変数、関数呼び出し時の戻りアドレスなどの一時データを保管するために使用されます。**関数が呼び出されると、そのパラメータとローカル変数がスタックに割り当てられ、関数が戻ると、これらのデータはスタックから削除されます。したがって、スタックは、関数の呼び出し時と戻り時にデータを管理する便利な方法を提供します。
スタックは通常、オペレーティング システムによって自動的に管理されます。オペレーティング システムはポインタを使用してスタックの先頭の位置を追跡します。データをスタックにプッシュする必要がある場合、ポインタは下に移動します。データをスタックの先頭からポップする必要がある場合は、ポインタが下に移動します。スタックに移動すると、ポインタが上に移動します。通常、スタックのサイズは固定されているため、スタックがオーバーフローするとプログラムはクラッシュします。したがって、プログラムを作成するときは、スタック オーバーフローの問題を回避するためにスタックのサイズを注意深く管理する必要があります。

(1) スタックの構造

ARM アーキテクチャの Cortex-m3 コアの場合、スタックは下方向に成長するフルスタックです、つまり、スタック ポインタ sp は最初、ユーザーが設定した上位アドレス (0x20001000 など) を指します。の場合、スタックの最上位ポインタが指すアドレスは最新のデータが配置されているアドレスであり、スタックの最下位ポインタが指すアドレスは最も古いデータが配置されているアドレスです。スタックがいっぱいになると、スタックの最上位ポインタはスタックの最下位ポインタが配置されているアドレスを指すことになり、この時点でデータを追加するとスタックがオーバーフローします。
下方向に成長するスタック「スタック ポインタ」レジスタの値はスタックの先頭のアドレスを指し、その値はスタックがプッシュされるたびにプッシュされたデータのサイズを減算するため、下方向に成長します。スタックの先頭にあるデータがポップされると、スタック ポインタの値が増加し、上に移動します。

(2) スタックオーバーフロー

ここに画像の説明を挿入
フルスタックは下方向に生成され、初期化された関数で定義された変数が大きすぎる場合、スタックポインタは下に進み続けます。スタックがフルになると、スタックの先頭ポインタはスタックの最下位ポインタのアドレスを指します。この時点でデータを追加するとスタック オーバーフローが発生します。

スタック オーバーフローが発生すると、次のような結果が発生します。
プログラム クラッシュ: スタック オーバーフローが発生すると、必然的にプログラムがクラッシュし、データが失われたり、プログラムが実行を継続できなくなる可能性があります。
データ破損: スタック オーバーフローによりデータが範囲外になるため、スタック内のデータが上書きまたは破損し、プログラムが正常に動作しなくなる可能性があります。
セキュリティの脆弱性: スタックのオーバーフローもセキュリティの脆弱性を引き起こす可能性があり、攻撃者が悪意のあるデータをスタックに追加することでプログラムの実行フローを制御し、悪意のあるコードを実行することができます。
メモリ リーク: プログラム内にスタック オブジェクトがある場合、スタックがオーバーフローしたときにこれらのオブジェクトが正しく解放されず、メモリ リークが発生する可能性があります。

(3) スタックオーバーフローを回避する方法: スタックのサイズを見積もる

目標は、 「最も多くのローカル変数を使用する」呼び出しチェーンを見つけることです。GPT からの回答は次のとおりです。
1. 関数呼び出しチェーンを観察します。スタックのサイズは、関数呼び出しの深さと各関数のローカル変数のサイズによって異なります。したがって、プログラム内の関数呼び出しチェーンとローカル変数のサイズを観察することで、スタックのサイズを大まかに見積もることができます。
2. 関数のパラメータと戻り値のサイズを見積もる: 関数のパラメータと戻り値もスタック領域を占有します。したがって、関数のパラメーターと戻り値のサイズを見積もることは、スタックのサイズを見積もるのに役立ちます。
3. 再帰を考慮する: プログラムに再帰関数がある場合は、再帰の深さとローカル変数のサイズを考慮してスタックのサイズを見積もる必要があります。
4. 例外処理を考慮する: プログラムで例外処理が使用されている場合は、例外処理のスタック サイズを考慮してスタック サイズを見積もる必要があります。
5. スタック オーバーフロー検出ツールを使用する: 一部のコンパイラとデバッグ ツールはスタック オーバーフロー検出機能を提供しており、これらのツールを使用してプログラム内で発生する可能性のあるスタック オーバーフローの問題を検出し、それによってスタックのサイズを最適化できます。

4. ヒープとは何ですか?

GPT の説明:
ヒープ (ヒープ) は、動的にメモリを割り当てる方法の 1 つで、オペレーティング システムがプログラムを実行するためにメモリを割り当てる領域であり、プログラムの実行中に動的に割り当てる必要があるデータを格納するために使用されます。ヒープメモリはプログラマ自身によって管理され、プログラマはオペレーティングシステムやプログラミング言語が提供するヒープ管理関数(malloc、newなど)を呼び出すことでヒープメモリの割り当てや解放を行うことができます。ヒープ メモリのサイズは通常動的であり、プログラムのニーズに応じて拡大および縮小できます。
ヒープ メモリの特徴は次のとおりです。
動的割り当て: プログラムのニーズに応じてヒープ メモリを動的に割り当てることができ、プログラマは実際の状況に応じて必要なメモリ サイズを割り当てることができます。
プログラマ管理: ヒープ メモリの管理はプログラマの責任であり、プログラマはヒープ メモリの割り当てと解放を行うために手動でヒープ管理関数を呼び出す必要があります。
マルチスレッド共有: ヒープ メモリは複数のスレッドで共有できるため、スレッドの安全性を考慮する必要があります。
メモリリーク: ヒープメモリの管理はプログラマの責任であるため、メモリリークのリスクがあり、プログラマは使用されなくなったヒープメモリを適時に解放するよう注意する必要があります。
メモリの断片化: ヒープ メモリの動的な割り当てと解放はメモリの断片化の問題を引き起こしやすいため、プログラマはメモリの断片化の問題を回避するために何らかの最適化措置を講じる必要があります。
ヒープ メモリは通常、動的なデータ構造 (リンク リスト、ツリーなど) や大量のデータ (画像、音声など) を保存するために使用されます。これは、これらのデータのサイズが通常不確実であり、メモリが必要なためです。ストレージに動的に割り当てられます。

1. サンプル機能

#include "stdio.h"
#include "stdlib.h"

int heap(){
    
    
char *str;
str = malloc(100);
strcpy(str,"helloworld");
free(str);
}

関数の場合、内部の変数定義ステートメントが実行されると、変数の空間が解放されることがわかります。指定したスタックの先頭から末尾までは、まだ大きな領域が残っていますが、スタックが作成されていない状態では空き領域となっているので、malloc ()で空き領域を指定すると、この空き領域が影響なし スタック領域の割り当ては下部のグローバル変数と重複しないため、一部の動的メモリ領域割り当て操作を完了できます。

2. char *str; と char str; の違い

char str は文字ポインタ変数を宣言します。つまり、 str はメモリ アドレスを指し、文字配列へのポインタを格納します。つまり、str はポインタです。

そして、 char str は文字配列変数を宣言します。つまり、 str は文字配列の内容を格納します。

具体的には、**char *str は、通常、malloc 関数を使用して文字列スペースを割り当て、そのアドレスを str に割り当て、その後 str ポインターoperate** を通じて文字列を処理する場合など、メモリ空間の動的な割り当てが必要な場合に使用されます。また、char str は通常、固定長の文字配列を宣言するために使用されます。たとえば、char str[100] は、最大 100 文字を格納できる長さ 100 の文字配列を宣言することを意味します。
例えば:

// 使用 char *str 定义字符串变量
char *str = (char*) malloc(100 * sizeof(char)); // 动态分配内存空间
strcpy(str, "Hello, world!"); // 复制字符串
printf("%s\n", str); // 输出字符串
free(str); // 释放内存空间

// 使用 char str[100] 定义字符串变量
char str[100] = "Hello, world!"; // 直接初始化字符数组
printf("%s\n", str); // 输出字符串

3. malloc() と free()

malloc と free は、C 言語の動的メモリ割り当ておよび解放関数です。

malloc 関数は、ヒープ メモリ内に指定されたサイズのメモリ領域を動的に割り当て、この領域へのポインタを返すために使用されますその関数プロトタイプは次のとおりです。

void *malloc(size_t size);

このうち、size パラメータは、割り当てられるメモリ空間のサイズをバイト単位で示します。malloc 関数は、割り当てられたメモリ空間の開始アドレスを指す void* タイプのポインタを返します。割り当てに失敗した場合は、ヌルポインタ NULL が返されます。

free 関数は、 malloc 関数によって割り当てられたメモリ空間を の前に解放するために使用され、その関数プロトタイプは次のとおりです。

void free(void *ptr);

このうち、ptrパラメータは解放するメモリ空間のポインタ、つまりmalloc関数が返すポインタを表します。free 関数を呼び出した後、メモリ空間が解放され、他の変数が使用できるように再割り当てできます。

要約すると、ヒープは空きメモリの一部であり、malloc または free 関数を使用して管理できます。

5. スタックがよくまとめて呼ばれるのはなぜですか? ヒープとスタックの違いは?

ヒープとスタックはコンピュータのメモリ管理において重要な 2 つのデータ構造であり、メモリ空間の管理に使用されるため、よく一緒に言及されます。
ただし、ヒープとスタックは異なるものであり、一緒にスタックと呼ぶべきではないと思います。ヒープはヒープ、スタックはスタックです。

ヒープとスタックの違い
1. 割り当て方法: スタック領域はシステムによって自動的に割り当ておよび管理されますが、ヒープ領域はプログラマが手動で割り当ておよび解放する必要があります。
2. スペース サイズ: スタック スペースのサイズは固定されており、システムによって事前に設定されますが、ヒープ スペースのサイズは固定されておらず、動的に割り当ておよび解放できます。
3. 割り当て効率:スタック領域の割り当てと解放はシステムによって自動的に行われるため、ヒープ領域に比べて割り当てと解放の効率が高くなります。
4. 格納方法: スタック領域ではデータの格納に後入れ先出し (LIFO) 方式が使用され、主にパラメータ、ローカル変数、関数呼び出し時の関数戻りアドレスなどの情報を格納するために使用されますが、ヒープ領域にはデータが格納されません。動的配列、リンク リストなど、動的に割り当てられたデータ構造を保存するために使用される特定のストレージ モード。
5. ライフサイクル: スタック領域のライフサイクルは関数呼び出しのライフサイクルと同じであり、関数呼び出し終了後、スタック領域のデータは自動的に解放されますが、ヒープ領域のライフサイクルは手動で管理されます。ヒープ領域を使用する場合は、プログラマが手動で領域を解放します。

6. 静的変数はスタックにプッシュされますか?

しません。
メモリ内の静的変数の格納場所は、スタックではなく、プログラムのデータ セグメント (データ セグメント) 内にあります。したがって、静的変数はスタックにプッシュされません。
**プログラムのデータ領域は、プログラムの実行時にシステムによって割り当てられるメモリ領域であり、グローバル変数、静的変数、定数などのデータを格納するために使用されます。**データ領域のサイズは固定されており、コンパイル時に決定されるため、静的変数の空間サイズも固定されており、関数呼び出しによって動的に変更されることはありません。
対照的に、スタックは、関数が呼び出されたときにパラメーター、ローカル変数、戻りアドレスなどの情報を保存するために使用される動的メモリ空間ですスタック領域のサイズは限られており、通常はわずか数百 KB ~ 数 MB であるため、スタックに格納される領域サイズと変数の数はある程度制限されます。

7. ヒープが特定のスペースにあることを指定するにはどうすればよいですか?

指定したアドレス セグメントにヒープ領域を割り当てる必要がある場合は、ポインタ型変換やポインタ演算などの手法を使用できます。

以下は、10 個の整数変数のサイズのヒープ領域を手動で割り当て、それを指定されたアドレス セグメントに割り当てる方法を示すサンプル コードです。

#include <stdio.h>
#include <stdlib.h>

int main() {
    
    
    int *ptr, *start_addr;
    int size = 10;
    // 分配堆空间
    ptr = (int*)malloc(size * sizeof(int));
    
    // 将堆空间分配到指定的地址段
    start_addr = (int*)0x10000; // 假设指定地址为 0x10000
    ptr = start_addr;
    
    // 释放堆空间
    free(ptr);
    return 0;
}

上記のコードでは、最初に 10 個の整数変数のヒープ領域が malloc 関数によって割り当てられ、次にポインタ ptr がこのヒープ領域の開始アドレスを指します。次に、ポインタ ptr が指定されたアドレス セグメントを指すことによって、ヒープ領域が指定されたアドレス セグメントに割り当てられます。ヒープ領域はfree関数によって解放されます。

おすすめ

転載: blog.csdn.net/qq_53092944/article/details/131029319