ARMアーキテクチャとC言語(魏東山)学習記(1)~C言語の本質~


序文

Bステーションで魏東山のARMアーキテクチャとC言語の勉強ノートを学びましょう。


1. C言語の簡単な操作

#include "stdio.h"
int main(){
    
    
    int a=1;
    a++;
    return 0;
}

a++ ステップを完了するために、3 つのステップが実行されます。
① ADDR の読み取り: CPU はメモリ内の変数 a のアドレスを読み取り、その中のデータ (a の値データ) を取得します。CPU はデータを読み取った後、そのデータを CPU の内部ストレージ構造 (R0 などのレジスタ) に保存します。

② CPU 内の演算器 ALU が R0 の値の累積演算を完了します。

③ADDR書き込み:メモリ上の変数aのアドレスにR0の値を書き込みます。

2. CPU はどのようにして命令の実行を認識するのか

1. FLASHの導入

コードを FLASH に書き込むと、電源がオンになると、CPU が FLASH 内の命令を取り出して実行します。次の図に示すように、a++ を例にします: a++; 実際にアセンブリ言語に変換され
ここに画像の説明を挿入
、最初:
①LDR R0,[addrA]: アドレスの読み取り A のデータがレジスタ R0 にロードされます。つまり、Load
②ADD R0, #1: レジスタ R0 の値に 1 を加算します。
③STR R0, [addrA]: の値を書き込みます。 R0をアドレスAに、つまりストアを保存し、
ROMに属しているためFLASHを使用してドロップすることになりますが、電気はまだデータを保存できるため、命令が失われることはありません。

KEIL によって生成された .HEX ファイルは、マイクロコントローラーのフラッシュにプログラムされます。

2. Cortex-m3コアの下のレジスタ

ここに画像の説明を挿入
(1) 汎用レジスタ (R0 ~ R12): データ操作の命令を呼び出すために使用されます。
(2)スタックポインタレジスタ(SP):スタックレジスタは、データの先入れ後出しを実現するためのスタックポインタ機能として使用される。
(3) 接続レジスタ (LR): リターン アドレス レジスタ。サブルーチンが呼び出されると、返されたアドレスは接続レジスタに直接格納されます。
(4) プログラム カウント レジスタ (PC): 次に実行される命令のアドレスを格納するために使用されます。
(5) 特殊機能レジスタ グループは 3 つのカテゴリに分かれています。 1. プログラム ステータス ワード レジスタ: ALU フラグの実行ステータスと現在処理されている割り込み番号を記録するために使用されます。2. 割り込みマスクレジスタ: 割り込みを無効にします。3. 制御レジスタ: 特権状態を定義し、どのスタック ポインタを使用するかを決定します。

3. 変数とは何ですか?

1. 変数 — 変化する可能性のある量

変数の最大の特徴は、変数がメモリ上に配置されることを決定する読み取りと書き込みができることです。

変数をメモリに格納するのは、メモリは記憶容量が大きく、読み書き速度が速いため、プログラム内のデータの格納や読み出しが容易であるためです。対照的に、レジスタの容量は小さく、少量のデータしか保存できず、読み書き速度は速いですが、レジスタへのアクセス速度はメモリへのアクセス速度よりも高速です。したがって、プログラムの実行効率を向上させるために、プログラム内の変数は通常、最初にメモリに格納され、次に操作のためにレジスタにロードされます。しかし、FLASH に保存されるのはプログラムコードと定数データであり、可変データの保存には適しておらず、読み書きは可能ですが、書き込みは複雑になります。

2. グローバル変数とローカル変数

グローバル変数:関数の外で定義された変数は グローバル変数 と呼ばれ、そのスコープは定義された場所からファイルの最後までとなります。グローバル変数はプログラム全体からアクセスできるため、ライフサイクルも非常に長く、プログラムが終了するまで破棄されません。グローバル変数の値はプログラム内のどこでも変更できるため、不要なエラーを避けるために注意して使用する必要があります。

ローカル変数:関数内で定義された変数はローカル変数と呼ばれ、その範囲はそれが定義されている関数に限定されます。ローカル変数は関数が呼び出されるたびに再初期化され、関数の実行が終了するとローカル変数の値は破棄されます。ローカル変数のスコープは関数内のみであるため、変数名の競合や不要なグローバル変数を回避できます。
コードを見てください:

#include "stdio.h"
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;
}

コード セグメントでは、ここで説明する 2 つのキーワードが使用されます。

①静的キーワード

ローカル変数には static を使用します:関数内で static で修飾された変数は、 static ローカル変数と呼ばれます。この変数は 1 回だけ初期化され、関数の終了時に破棄されません。値は引き続き維持され、次回関数が呼び出された場合、変数の値はリセットされません。静的ローカル変数のスコープは関数内のみであり、他の関数には影響しません。

グローバル変数には static を使用する:関数の外側で static で装飾された変数はstatic グローバル変数と呼ばれ、このファイル内の関数からのみアクセスでき、他のファイルからはアクセスできません。静的グローバル変数のスコープはこのファイル内のみであり、他のファイルには影響しません。

関数には static を使用する: 関数の外側で static で装飾された関数はstatic 関数と呼ばれます。この関数は、このファイル内の他の関数からのみ呼び出すことができ、他のファイルから呼び出すことはできません。静的関数のスコープはこのファイル内のみであり、他のファイルには影響しません。

現在のファイルとは、静的変数を定義するソース コード ファイル (.c ファイルや .cpp ファイルなど) を指します。このソース ファイルでは、どの関数でも静的変数にアクセスできますが、他のファイルで定義された関数はその変数にアクセスできません。これは、静的変数のスコープが現在のソース ファイルのスコープに制限されており、他のソース ファイルはこのスコープ内の変数にアクセスできないためです。

②揮発性キーワード

Volatile は C 言語のキーワードで、コンパイル中に変数を最適化しないようにコンパイラに指示し、変数にアクセスするたびにデータがメモリから読み取られるか、メモリに書き込まれるようにしてコンパイルを回避します。コンパイラは変数を最適化します。その結果、変数に予期しない値が返されます

マルチスレッドまたは割り込みハンドラーで、変数が複数のスレッドまたは割り込みハンドラーで共有される場合、データの一貫性を確保するために、 volatile キーワードを使用する必要があります。複数のスレッドまたは割り込みが同時に変数にアクセスする可能性があるため、volatile キーワードが使用されていない場合、コンパイラーが変数を最適化し、データの不整合が発生する可能性があります。

3. 機能分析

ここでは、KEIL と STM32 の STLINK デバッグ メソッドを使用し、プログラムをコンパイルしてデバッグします。次のことがわかります。
ここに画像の説明を挿入

1.揮発性整数

このローカル変数 a はコンパイラで最適化できないため、スタック上に一時的に保存されます。では、スタックとは何でしょうか?

スタック (Stack) はデータ構造であり、一端でのみ挿入および削除が可能な線形テーブルです。スタックは「先入れ後出し」の原則に基づいて動作します。つまり、最後に挿入された要素が最初に削除されます。スタックに要素を挿入する操作を「プッシュ」といい、スタックから要素を削除する操作を「ポップ」といいます。

コンピュータでは、スタックとは通常、プログラムの実行時に使用されるメモリのセクションを指し、ローカル変数、関数パラメータ、関数呼び出し時のリターン アドレスなどの情報を格納するために使用されます。関数が呼び出されるたびに、情報を格納するためのスペースがスタック上に割り当てられます。関数が戻ると、情報はスタックからポップされ、スタック ポインタは前の関数のスタックの先頭に戻り、前の関数の実行が継続されます。スタックの特性は後入れ先出しであるため、関数が呼び出されると、新しい関数は最初にスタックにプッシュされ、実行が完了するまでポップされません。
STM32F103 のメモリ構造は次のとおりです。
ここに画像の説明を挿入ここに画像の説明を挿入
したがって、明らかに内部 SRAM 領域でスタック空間が開かれ、境界は 0x20000000 ~ 0x3FFFFFFF ベース アドレス 0x20000000 です。SRAM 領域にはグローバル変数やスタティック変数などを格納する必要があるほか、ローカル変数を格納するためにスタック領域を開く必要があるため、プログラマは他の変数を区別するためにスタックの開始アドレスを設定するようにプログラムすることができます。

2. コンパイラの役割

コンパイラは、ソース コードをオブジェクト コードに変換するプログラムです。プログラマーが書いたソース コード (C、C++、Java など) を、コンピューターが理解して実行できるマシン コードに変換できます。コンパイラは通常、プリプロセッサ、字句アナライザ、構文アナライザ、セマンティック アナライザ、オプティマイザ、コード ジェネレータなどを含む複数のモジュールで構成されます。

コンパイラの主な機能は次のとおりです。

ソース コードをオブジェクト コードに変換する: コンパイラは、プログラマが書いたソース コードを、コンピュータが理解して実行できるマシン コードに変換します。このプロセスには、字句分析、構文分析、意味分析、最適化、コード生成などの複数の段階が含まれます。
オブジェクト コードの最適化: コンパイラは、生成されたオブジェクト コードを最適化し、冗長なコードを削除し、コードの実行効率を向上させるなどして、プログラムをより高速に実行できるようにします。
コードの正確性をチェックする: コンパイラは、ソース コードの構文エラーや型エラーなどをチェックして、プログラムの正確性と信頼性を向上させることができます。
デバッグ情報の提供: コンパイラーは生成されたオブジェクト コードにデバッグ情報を追加できるため、プログラマーはプログラムのデバッグ時に分析およびトラブルシューティングを行うことができます。

つまり、コンパイラーは非常に強力であり、作成したコードは CPU 処理用のマシンコードに変換されます。

3. ローカル変数の代入と初期化

知識ポイント 1:
SP (スタック ポインタ): スタックの先頭アドレスを指すスタック ポインタ レジスタ。プログラムの実行中、スタックは、ローカル変数、関数パラメータ、関数呼び出し時のリターン アドレスなどの情報を格納するために使用される重要なデータ構造です。スタック ポインタ SP はスタックの先頭を指し、スタックのサイズはプログラムによって設定されます。
LR (リンク レジスタ): リンク レジスタは、ジャンプ命令のリターン アドレスを格納するために使用されます。関数が呼び出されると、関数が戻るときに LR レジスタにアドレスが保存されるため、関数の実行後に関数は正しいアドレスに戻ります。
PC (プログラム カウンタ): プログラム カウンタは、次に実行される命令のアドレスを格納するために使用されます。プログラムの実行中、PC は実行される次の命令を指すように常に更新されるため、プログラムは順次実行されます。

知識ポイント 2:
char: 1 バイト
short: 2 バイト
int: 4 バイト
length: 4 バイトまたは 8 バイト (コンパイラおよびオペレーティング システムに応じて)
float: 4 バイト
double: 8 バイト
length double: 8 バイトまたは 16 バイト (に応じて)コンパイラおよびオペレーティング システムに依存)
ポインタ タイプ: 4 バイト (32 ビット システム) または 8 バイト (64 ビット システム)

main 関数を変更し、文字配列を追加して、値を割り当てます。

int main(){
    
    
    static volatile int s_a=1;
    volatile int b = 456;
    volatile char name[100];
    name[0]='A';
    b=add_val(s_a);
    return 0;
}

(1) b と name に値を代入しない場合、コンパイラは巧みにスタック領域を割り当てません。
(2)

POP {r2-r3, pc}
MOVS r0, r0

POP {r2-r3, pc} この命令の機能は、スタックから 4 バイトをポップし、レジスタ r2、r3、およびプログラム カウンタ (pc) にそれぞれ格納することです。この命令は通常、スタックからシーンを復元し、関数が戻ったときに呼び出し元の関数の位置にジャンプするために使用されます。
MOVS r0、r0、この命令の機能は、レジスタ r0 の値をレジスタ r0 にコピーすることです。この命令は実質的な効果がないように見えますが、r0 の値を元の値に上書きするため、レジスタ r0 の値をクリアするために実際に使用できます。これは何もしないのと同じです。
2.

PUSH {lr}
SUB sp, sp, #0x68

PUSH {lr}、この命令の機能は、リンク レジスタ (lr) の値をスタックにプッシュすることです。この命令は通常、関数が呼び出されるときに使用され、関数の実行後に正しい位置に戻ることができるように、関数の戻りアドレスをスタックに保存します。
SUB sp, sp, #0x68
この命令の機能は、スタック ポインター (sp) から 0x68 を減算すること、つまり、スタックの先頭に 0x68 バイトのスペースを割り当てることです。この命令は通常、関数スタック フレームの割り当てに使用され、関数のローカル変数と一時データを保存するために使用されます。int 変数は 4 バイトを占め、100 文字で構成される配列は 100 バイトを占めます。0x68 は 10 進数で 104 なので、CPU はスタックの先頭アドレス (0x20010000 など) から 4 を減算し、LR の戻り値を返します。次に 104 を減算し、つまり 2 つのローカル変数に 104 バイトのスタック領域を割り当てます。
3.

MOV r0, #0x1C8
STR r0, {sp, #0x00}

MOV r0, #0x1C8
この命令の機能は、即値 0x1C8 (つまり、456 の 16 進表現) をレジスタ r0 に移動することです。この命令は通常、定数をレジスタにロードするために使用されます。

STR r0, {sp, #0x00}
この命令の機能は、レジスタ r0 の値をスタック ポインタ (sp) のアドレスに即値 0x00 を加えたものに格納することです。この命令は通常、レジスタの値をメモリに保存するために使用されます。ここでは、r0 の値がスタックの最初の位置に保存されます。b の値をメモリ内のアドレスに保存します。

4. ローカル変数の解放

ここに画像の説明を挿入
同様に、add_val(volatile int v) 関数が実行されると、CPU は別のスタックを作成し、即値をレジスタ r0 に移動し、r0 の値を sp が指すアドレス (sp が指すアドレス) に保存します。ポインタの変化)+自身のアドレス。
v=v+a の演算は、read-accumulate-write の演算を使用しますが、ここでは a がローカル変数、v が関数のパラメータであるため、比較的複雑です。最後に、r0 の値を sp が指すアドレスに読み取ります。
ここに画像の説明を挿入
ここで注意が必要なのは、PUSH命令はレジスタの値をスタックにプッシュする命令、POP命令はスタックからデータをポップしてレジスタに格納する命令であり、add_val(volatile int v)関数が実行されるため、CPUはスタックスペースがオープンされ、関数の終了後にスタックが解放される必要があり、pop 命令を使用してシーンを復元し、スペースを解放できます。sp レジスタは、メイン関数 main によって保存されたアドレスも指します。次の関数呼び出しを待っています。

4番目、cortex-m3のスタック

Cortex-m3 のスタックは下方向に成長するフルスタックです。

下方向に拡張するフルスタックとは、メモリ内に割り当てられた固定サイズのスタック領域が、上位アドレスから下位アドレスに拡張されることを指します。スタック領域がいっぱいになると、スタック オーバーフロー (Stack Overflow) エラーが発生し、プログラムがクラッシュしたり、異常な動作をしたりすることがあります。
下向き成長フルスタック モデルでは、スタック ポインタ (Stack Pointer) は現在のスタックの先頭を指し、スタックが使用されるにつれてスタックの先頭のアドレスは下に移動します。スタックの使用量がスタック領域のサイズを超えると、スタックの上部がスタックの下部を横切って他のメモリ領域を覆い、スタック オーバーフロー エラーが発生します。
下方向に成長するフルスタック モデルは通常、x86 アーキテクチャのコンピュータで使用されます。このアーキテクチャはリトル エンディアン (リトル エンディアン) を使用します。つまり、下位バイトは低いアドレスに格納され、上位バイトは上位アドレスの場所に保存されます。したがって、スタック スペースは上位アドレスから下位アドレスに増加し、これはコンピュータの記憶方式により一致します。
Cortex-m3のスタックは下に向かって成長するフルスタックで、アドレスは上から下に減っていきますが、spレジスタはまずポインタの位置を調整し、それを演算に格納します。
ここに画像の説明を挿入

PUSH で始まり POP で終わります。PUSHでは、まずspマイナス4を調整してr0に格納します。次に、sp マイナス 4 を調整し、LR に保存します。POP では、v の戻り値を r2 に与え、sp に 4 を加えます (つまり、ポップアップし、ポインタが上記の位置を指します)。次に、r0 の値を r3 に与え、sp に 4 を加え、最後にLR の値をPC に渡すことは、main 関数が add 関数に入るときに LR によって保存されたアドレスを PC に与えることです。CPU に main 関数に入って操作を続行させ、sp に 4 を加算して、元の関数に戻ります。スタックの最上位に配置し、add 関数のスタック領域を解放します。

おすすめ

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