組み込みソフトウェア エンジニアの古典的な筆記試験問題

1. 前処理ディレクティブ #define を使用して、1 年が何秒であるかを示す定数を宣言します (うるう年の問題は無視します)。

回答: この質問で主に間違いが起こりやすい部分は、この式が 16 ビット マシンの整数をオーバーフローさせることを認識しているため、この定数が長整数であることをコンパイラに伝えるために長整数記号 L が使用されているということです。

 #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL

2. 2 つの引数を受け取り、小さい方の引数を返す「標準」マクロ MIN を作成します。

回答: この質問で主に間違いが起こりやすい部分は、マクロ内でパラメータを括弧で慎重に囲む方法を知っていることです。

#define MIN(A,B) ((A)<=(B)?(A):(B)) 

もちろん、マクロを使用すると副作用もあります。この例を見てみましょう: MIN(*p++, b) に対するマクロ定義の効果は次のとおりです: ((*p++) <= (b) ? (*p++) : (b)) この式は副作用、ポインタ p を生成します。 2 つの ++ 自己インクリメント操作を実行します。

3. 変数 a を使用して次の定義を与えます。10 個のポインターを持つ配列、ポインターは関数を指し、関数は整数パラメーターを持ち、整数を返します。

回答: この質問の主な間違いは、関数ポインターとポインター配列です。

int (*a[10])(int);

4. キーワード static の機能は何ですか?

回答: C 言語では、キーワード static には 3 つの明らかな機能があります。

関数本体内で、静的として宣言された変数は、関数の呼び出し時に 1 回だけメモリが割り当てられ、実行時全体を通じて再割り当てされることはありません。関数本体の外またはソース ファイル内で、静的変数として宣言された変数は、関数の呼び出し時にのみメモリが割り当てられます
。ソース ファイル内のすべての関数からアクセスできますが、他のソース ファイル内の関数からはアクセスできません。これはローカル グローバル変数であり、
ソース ファイル内で静的に宣言された関数は、ソース ファイル内の他の関数からのみ呼び出すことができます。つまり、関数は、それが宣言されているソース ファイルのローカル スコープに制限されます。
5. キーワード const の機能は何ですか?

答え: 簡単に言えば、const は定数を意味します。

const で定義された変数、その値は変更できず、スコープ全体で固定されます。
マクロ定義と同様に、あいまいな数値の出現を回避でき、パラメータの調整や変更にも非常に便利です。
変更された内容を保護できます。偶発的な変更を防止し、プログラムの堅牢性を高めるためのもの。const は、コンパイラがコンパイル時にチェックを実行することによって保証されます。
定数とポインタ

次のステートメントは何を意味しますか:`

1.const int a;
2.int const a;
3.const int *a;
4.int * const a;
5.const int * const a;
6.int const * const a;`

最初の 2 つの関数は同じです。a は定数の整数です。

3 番目は、 a が定数整数へのポインタであることを意味します (つまり、整数は不変ですが、ポインタは不変である可能性があります)。4 番目は、 a が整数への定数ポインタ (つまり、ポインタが指す整数 ) であることを意味します
。は変更可能ですが、ポインタは変更可能ではありません);
最後の 2 つは、 a が定数整数への定数ポインタであることを意味します (つまり、ポインタが指す整数は変更可能ではなく、ポインタも不変です)。
定数と関数

const は通常、関数の引数に使用されますが、引数がポインタの場合、ポインタが指すデータが関数内で変更されるのを防ぐために、const を使用して制限することができます。たとえば、String プログラムには const で変更されたパラメータが多数あります。

void StringCopy(char* strDestination, const char *strSource);

const は、関数が定数を返し、その定数が関数の戻り値に置かれることを意味することもあります。例えば:

const char * GetString(void);

クラス メンバー関数の宣言と定義では、関数のパラメーター テーブルの後、関数本体の前に const が配置されます。これは、関数の this ポインターが定数であり、オブジェクトのデータ メンバーを変更できないことを示します。例えば:

void getId() const;

6. キーワード volatile の意味は何ですか? 3 つの異なる例が示されています。

回答: 変数が volatile として定義されているということは、変数が予期せず変更される可能性があるため、コンパイラーは変数の値を想定しません。正確に言うと、オプティマイザは、レジスタに格納されたバックアップを使用するのではなく、この変数を使用するたびにその値を注意深く再読み取る必要があります。次に、揮発性変数の例をいくつか示します。

メモリ マップされたハードウェア レジスタには、通常、voliate も追加されます。これは、レジスタへの読み取りと書き込みがそれぞれ異なる意味を持つ可能性があるためです。
割り込み関数の対話型変数は、各読み取りが自動的に保存されないように、volatile キーワードを使用して変更する必要があります。 type (グローバル変数、静的変数) はメモリ アドレスから読み取られ、それが必要なデータであることを確認します。
マルチタスク環境のタスク間で共有されるフラグは volatile に追加する必要があります。
パラメータは const と volatile の両方にすることができますか?

はい、たとえば読み取り専用ステータス レジスタです。予期せず変更される可能性があるため、不安定です。プログラムがそれを変更しようとしてはいけないため、const になっています。ソフトウェアが変えられないからといって、ハードウェアがあなたの価値を変えられないということではなく、これがワンチップマイコンのアプリケーションです。

参考記事:Cのvolatile - このままにしておきます。

ポインタは揮発性になる可能性がありますか?

できる。これはあまり一般的ではありませんが。例としては、サービス サブルーチンがバッファへのポインタを変更する場合が挙げられます。

次の関数のどこが間違っているのでしょうか:`

int square(volatile int *ptr)
{
    
    
        return *ptr * *ptr;
}

このコードの目的は、ポインタ ptr が指す値の 2 乗を返すことですが、ptr は揮発性パラメータを指しているため、コンパイラは次のようなコードを生成します。

`int square(volatile int *ptr) 
{
    
    
  	int a,b;
   	a = *ptr;
    b = *ptr;
    return a * b;
}`

*ptr の値は予期せず変更される可能性があるため、a と b が異なる場合があります。その結果、このコードは期待した平方値を返さない可能性があります。正しいコードは次のとおりです。

long square(volatile int *ptr) 
{
    
    
    int a;
    a = *ptr;
    return a * a;
}

7. 整変数 a を指定して、コードを 2 つ書きます。最初のコードは a のビット 3 を設定し、2 番目のコードは a のビット 3 をクリアします。

回答: この質問では、「&=~」メソッドを使用して、a のビット 3 をクリアします。

#define BIT3 (0x1 << 3)
static int a;
 
void set_bit3(void) 
{
    
    
    a |= BIT3;
}
void clear_bit3(void) 
{
    
    
    a &= ~BIT3;
}

8. 組み込みシステムには、プログラマが特定のメモリ位置にアクセスする必要がある機能が備わっていることがよくあります。あるプロジェクトでは、絶対アドレスが0x67a9~0xaa66の整数変数の値を設定する必要があります。

解決策: この質問は、絶対アドレスにアクセスするために整数 (絶対アドレス) をポインターにキャストすることが合法であるかどうかをテストします。

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;

9. 割り込みは組み込みシステムの重要な部分であるため、多くのコンパイラ開発者は拡張機能を提供し、標準 C で割り込みをサポートできるようにしました。代表的な事実は、新しいキーワード __interrupt が作成されたことです。次のコードでは、__interrupt キーワードを使用して割り込みサービス サブルーチン (ISR) を定義しています。このコードについてコメントしてください。

__interrupt double compute_area (double radius) 
{
    
    
    double area = PI * radius * radius;
    printf("/nArea = %f", area);
    return area;
}

回答: この関数には非常に多くのエラーがあるため、どこから始めればよいのかわかりません。

ISR はパラメータを渡すことができず、値を返すこともできません。
浮動小数点は通常、多くのプロセッサ/コンパイラではリエントラントではありません。一部のプロセッサ/コンパイラはスタックの先頭でレジスタをプッシュする必要があり、一部のプロセッサ/コンパイラは ISR での浮動小数点演算を許可しません。さらに、ISR は短く効率的である必要があり、ISR で浮動小数点演算を行うのは賢明ではありません。printf
(char * lpFormatString,…) 関数は再入可能性とパフォーマンスの問題を引き起こすため、ISR では使用できません。
これらの要件の説明:

a. 戻り値がないのはなぜですか?

割り込みサービス関数の呼び出しはハードウェア レベルで行われます。割り込みが生成されると、pc ポインタは対応する割り込みサービス関数エントリに強制的にジャンプします。割り込みへの入力はランダムであり、特定の部分によって呼び出されるわけではありません。コードです。戻り値がある場合、その戻り値は誰に返されますか? 明らかに、この戻り値には意味がありません。戻り値がある場合は、スタックにプッシュする必要があるため、いつ、どのようにスタックをポップするかが決まります。解決不能になる。

b. ISR にパラメーターを渡すことができませんか?

同様に、パラメータを渡す関数には確実にプッシュおよびポップ操作が必要になるため、このようにスタックが破壊される理由でもあります。割り込みサービス関数のランダムな行に入るために、これは問題になります。誰でもパラメータを渡すことができます。

c. ISR はできるだけ短く簡潔にする必要がありますか?

割り込みが頻繁に生成され、それに対応する ISR に非常に時間がかかる場合、割り込みに対する応答は無限に遅延し、多くの割り込み要求が失われます。

d.printf(char * lpFormatString,...) 関数は再入性とパフォーマンスの問題を引き起こすため、ISR では使用できません。

これには割り込みの入れ子問題が関係します。printf などの glibc 関数はバッファー機構を使用するため、このバッファーは共有され、グローバル変数に相当します。最初の層の割り込みが来ると、この時点でコンテンツの一部が書き込まれます。 、優先度の高い割り込みが来て、printf も呼び出され、バッファーに内容も書き込まれたため、バッファーの内容がめちゃくちゃになりました。

リエントラント関数は主にマルチタスク環境で使用されます。リエントラント関数は単に中断できる関数です。つまり、この関数の実行中にいつでも中断でき、実行のために OS のスケジューリングに転送できます。コードの一部制御を返すときにエラーは発生しません。非リエントラント関数は、グローバル変数領域、割り込みベクタ テーブルなどの一部のシステム リソースを使用するため、割り込みが発生すると問題が発生する可能性があります。環境。

割り込みサービス関数と関数呼び出しの関係と違い:

接続: どちらもブレークポイント (つまり、次の命令アドレス) の保護、サブルーチンまたは割り込みサービス ルーチンへのジャンプ、コンテキストの保護、サブルーチンまたは割り込み処理、コンテキストの復元、ブレークポイントの復元 (つまり、メイン プログラムに戻る) が必要です。両方とも入れ子にすることができます。つまり、実行中のサブルーチンが別のサブルーチンに転送されるか、処理中の割り込みプログラムが別の新しい割り込み要求によって中断されます。入れ子は複数レベルにすることができます。

違い: 両者の基本的な違いは、主にサービス時間とサービス対象の違いに反映されます。まず、サブルーチン処理を呼び出す時刻、つまりメインプログラム内の呼び出しコマンド(CALL)が実行されると、メインプログラムがサブルーチンを呼び出し、その呼び出しコマンドの位置が既知かつ固定されている。割り込み処理が発生するタイミングは一般にランダムであり、CPUがあるメインプログラム実行時に割り込みソースから割り込みアプリケーションを受け取ると、割り込み処理が発生しますが、通常、割り込みアプリケーションはハードウェア回路によって生成され、そのアプリケーション時間はソフト割り込みの発生時間はランダムであり、サブルーチンの呼び出しはプログラム設計者によって事前に調整され、割り込みサービスプログラムの実行はシステムの動作環境によってランダムに決定されるとも言えます。

次に、サブルーチンは完全にメインプログラムの役割を果たし、両者は主従関係にあり、メインプログラムがサブルーチンを必要とする場合にはサブルーチンを呼び出し、呼び出し結果をメインプログラムに戻して実行を継続します。割り込みサービス プログラムとメイン プログラムは一般に無関係であり、誰が誰にサービスを提供するかについては問題がなく、この 2 つは並行しています。

第三に、メインプログラムによるサブルーチンの呼び出しプロセスは完全にソフトウェア処理プロセスであり、特別なハードウェア回路を必要としませんが、割り込み処理システムはソフトウェアとハ​​ードウェアの組み合わせであり、割り込み処理プロセスを完了するために特別なハードウェア回路を必要とします。

第 4 に、いくつかのレベルのサブルーチン ネストを実現できます。ネスト レベルの最大数はコンピュータ メモリによって開かれるスタック サイズによって制限されますが、割り込みのネスト レベルの数は主に割り込みの優先順位の数によって決まります。優先レベルの数はそれほど多くありません。

10. 次のコードの出力は何ですか?またその理由は何ですか?

void foo(void)
{
    
    
    unsigned int a = 6;
    int b = -20;
    (a+b > 6) ? puts("> 6") : puts("<= 6");
}

解決策: この質問では、C 言語の整数自動変換の原理を理解しているかどうかをテストしますが、一部の開発者はこれらのことをほとんど理解していないことがわかりました。とにかく、符号なし整数の問題の答えは、出力が「">6」であるということです。その理由は、式に符号付き型と符号なし型が存在する場合、すべてのオペランドが自動的に符号なし型に変換されるためです。したがって、-20 は非常に大きな正の整数になるため、式は 6 より大きく評価されます。

もう 1 つ重要な知識ポイントがあります。

通常、int型の値を演算する場合、CPUの演算速度が最も速くなります。x86 では、32 ビット演算は 16 ビット演算の 2 倍高速です。C 言語は効率を重視した言語であるため、プログラムをできるだけ速く実行するために整数昇格を実行します。したがって、整数のオーバーフローの問題を回避するには、整数のプロモーション ルールを覚えておく必要があります。

整数昇格とは、C 言語の規定で、式(比較演算、算術演算、代入演算などを含む)を計算する際に、int 型より小さい型(char、signed char、unsigned char、short、unsigned short など)を使用しないでください。 .) は、最初に int 型にプロモートされてから、式の演算を実行する必要があります。

昇格方法としては、元の型に応じたビット拡張(元の型がunsigned charの場合はゼロ拡張、元の型がsigned charの場合は符号ビット拡張)を32ビットに行う。つまり、次のようになります。

unsigned char の場合は、ゼロ拡張を実行します。つまり、左側の上位ビットを 0 ~ 32 ビットで埋めます。signed
char の場合は、符号ビット拡張を実行します。符号ビットが 0 の場合、左上位ビットは 0 ~ 32 ビットで埋められ、符号ビットが 1 の場合、左上位ビットは 1 ~ 32 ビットで埋められます。
11. 次のコード スニペットを評価します。

unsigned int compzero = 0xFFFF;

解決策: int 型が 16 ビットではないプロセッサの場合、上記のコードは正しくありません。次のように書く必要があります。

unsigned int compzero = ~0;

12. 非組み込みコンピュータほど一般的ではありませんが、組み込みシステムには依然としてヒープからメモリを動的に割り当てるプロセスがあります。では、組み込みシステムでメモリを動的に割り当てる場合に考えられる問題は何でしょうか?

回答: 動的割り当てでは、必然的に問題が発生します。

メモリ リーク: メモリ リークは、通常、プログラム自体のコーディングの欠陥によって引き起こされます。共通の malloc メモリの後に解放または他の同様の操作はありません。システムは実行プロセス中に繰り返し malloc を実行し、システム メモリを使い果たし、カーネル OOM を引き起こします。特定のプロセスは、強制終了して終了するためのメモリを適用する必要があります。
メモリの断片化: メモリの断片化はシステムの問題であり、malloc と free が繰り返され、解放後のメモリはシステムによってすぐにリサイクルできません。これは、動的にメモリを割り当てる割り当てアルゴリズムにより、これらの空きメモリが使用できなくなるためであり、これらの空きメモリがさまざまな場所に小規模かつ離散的に出現するためにこの問題が発生します。
以下のコード スニペットの出力は何ですか?またその理由は何ですか?

char *ptr;
if ((ptr = (char *)malloc(0)) == NULL) 
    puts("Got a null pointer");
else
    puts("Got a valid pointer");

関数 malloc() のパラメータは 0 にすることができます。

13. Typedef は、C 言語で既存のデータ型の同義語を宣言するためによく使用されます。プリプロセッサでも同様のことを行うことができます。たとえば、次の例を考えてみましょう。

#define dPS struct s *
typedef struct s * tPS;

上記の両方のケースの目的は、 dPS と tPS を構造体 s へのポインターとして定義することです。どの方法が良いでしょうか?

答え: typedef の方が優れています。次の例を考えてみましょう。

dPS p1,p2;
tPS p3,p4;

最初の定義の拡張である場合:

struct s * p1, p2;

p1 はポインタ、p2 は構造体です。明らかに、私たちが望んでいる答えではありません。

おすすめ

転載: blog.csdn.net/qizhi321123/article/details/131474717