1. 程式人生 > >【嵌入式基礎】嵌入式軟體開發——嵌入式軟體工程師經典筆試題

【嵌入式基礎】嵌入式軟體開發——嵌入式軟體工程師經典筆試題

從CSDN各個部落格上摘選的一些容易做錯的嵌入式軟體的筆試題,做一下記錄,讓自己記住。

1、用預處理指令#define 宣告一個常數,用以表明1年中有多少秒(忽略閏年問題)

解答:這一題主要容易錯的地方就是:意識到這個表示式將使一個16位機的整型數溢位,因此要用到長整型符號L,告訴編譯器這個常數是的長整型數。

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

2、寫一個"標準"巨集MIN,這個巨集輸入兩個引數並返回較小的一個。

解答:這一題主要容易錯的地方就是:懂得在巨集中小心地把引數用括號括起來。

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

當然,使用巨集也是有副作用的。就拿這一個例子來說:該巨集定義對MIN(*p++, b)的作用結果是:((*p++) <= (b) ? (*p++) : (b)) 這個表示式會產生副作用,指標p會作兩次++自增操作。

3、用變數a給出下面的定義:一個有10個指標的陣列,該指標指向一個函式,該函式有一個整型引數並返回一個整型數。

解答:這一道題主要容易錯的地方就是:函式指標、指標陣列。

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

4、關鍵字static的作用是什麼?

解答:在C語言中,關鍵字static有三個明顯的作用:

  • 在函式體中,一個被宣告為靜態的變數在這一函式被呼叫過程中只會被分配一次記憶體,且整個執行期間不會重新分配;
  • 在函式體外、某個原始檔內,一個被宣告為靜態的變數只可被該原始檔內的所有函式訪問,但不能被其他原始檔的函式訪問。它是一個本地的全域性變數;
  • 在某個原始檔內,一個被宣告為靜態的函式僅僅只可以被這個原始檔的其它函式呼叫。也就是說,這個函式被限制在宣告它的原始檔的本地範圍之內使用。

5、關鍵字const的作用是什麼?

解答:簡單地說,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;

  • 前兩個的作用是一樣,a是一個常整型數;
  • 第三個意味著a是一個指向常整型數的指標(也就是,整型數是不可修改的,但指標可以);
  • 第四個意思a是一個指向整型 數的常指標(也就是說,指標指向的整型數是可以修改的,但指標是不可修改的);
  • 最後兩個意味著a是一個指向常整型數的常指標(也就是說,指標指向的整型數 是不可修改的,同時指標也是不可修改的)。

const與函式

  • const 通常用在函式形參中,如果形參是一個指標,為了防止在函式內部修改指標指向的資料,就可以用 const 來限制。比如在String的程式中有很多const修飾形參的情況:
void StringCopy(char* strDestination, const char *strSource);
  • const還可以表示該函式返回一個常量,放在函式的返回值的位置。比如:
const char * GetString(void);
  • 在類成員函式的宣告和定義中,const放在函式的引數表之後,函式體之前,表示該函式的this指標是一個常量,不能修改該物件的資料成員。比如:
void getId() const;

6、關鍵字volatile有什麼含意?並給出三個不同的例子。

解答:一個定義為volatile的變數是說這變數可能會被意想不到地改變,這樣,編譯器就不會去假設這個變數的值了。精確地說就是,優化器在用到這個變數時必須每次都小心地重新讀取這個變數的值,而不是使用儲存在暫存器裡的備份。下面是volatile變數的幾個例子:

  • 儲存器對映的硬體暫存器通常也要加voliate,因為每次對它的讀寫都可能有不同意義;
  • 在中斷函式中的互動變數,一定要加上volatile關鍵字修飾,這樣每次讀取非自動儲存型別的值(全域性變數,靜態變數)都是在其記憶體地址中讀取的,確保是我們想要的資料;
  • 多工環境下各任務間共享的標誌應該加volatile。

一個引數既可以是const還可以是volatile嗎?

可以的,例如只讀的狀態暫存器。它是volatile因為它可能被意想不到地改變。它是const因為程式不應該試圖去修改它。軟體不能改變,並不意味著我硬體不能改變你的值,這就是微控制器中的應用。

一個指標可以是volatile 嗎?

可以。儘管這並不很常見。一個例子是當一箇中服務子程式修該一個指向一個buffer的指標時。

下面的函式有什麼錯誤:

  1. int square(volatile int *ptr)

  2. {

  3. return *ptr * *ptr;

  4. }

這段程式碼的目的是用來返指標*ptr指向值的平方,但是,由於*ptr指向一個volatile型引數,編譯器將產生類似下面的程式碼:

  1. int square(volatile int *ptr)

  2. {

  3. int a,b;

  4. a = *ptr;

  5. b = *ptr;

  6. return a * b;

  7. }

由於*ptr的值可能被意想不到地該變,因此a和b可能是不同的。結果,這段程式碼可能返不是你所期望的平方值!正確的程式碼如下:

  1. long square(volatile int *ptr)

  2. {

  3. int a;

  4. a = *ptr;

  5. return a * a;

  6. }

7、給定一個整型變數a,寫兩段程式碼,第一個設定a的bit3,第二個清除a的bit3。

解答:這道題清除a的bit3,使用“&=~”的方法。

  1. #define BIT3 (0x1 << 3)

  2. static int a;

  3. void set_bit3(void)

  4. {

  5. a |= BIT3;

  6. }

  7. void clear_bit3(void)

  8. {

  9. a &= ~BIT3;

  10. }

8、嵌入式系統經常具有要求程式設計師去訪問某特定的記憶體位置的特點。在某工程中,要求設定一絕對地址為0x67a9的整型變數的值為0xaa66。

解答:這一問題測試你是否知道為了訪問一絕對地址,把一個整型數(絕對地址)強制轉換為一指標是合法的。

  1. int *ptr;

  2. ptr = (int *)0x67a9;

  3. *ptr = 0xaa55;

9、中斷是嵌入式系統中重要的組成部分,這導致了很多編譯開發商提供一種擴充套件——讓標準C支援中斷。具代表事實是,產生了一個新的關鍵字 __interrupt。下面的程式碼就使用了__interrupt關鍵字去定義了一箇中斷服務子程式(ISR),請評論一下這段程式碼的。

  1. __interrupt double compute_area (double radius)

  2. {

  3. double area = PI * radius * radius;

  4. printf("/nArea = %f", area);

  5. return area;

  6. }

解答:這個函式有太多的錯誤了,以至讓人不知從何說起了:

  • 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在執行某一主程式時收到中斷源提出的中斷申請時,就發生中斷過程,而中斷申請一般由硬體電路產生,申請提出時間是隨機的(軟中斷髮生時間是固定的),也可以說,呼叫子程式是程式設計者事先安排的,而執行中斷服務程式是由系統工作環境隨機決定的;

其次,子程式完全為主程式服務的,兩者屬於主從關係,主程式需要子程式時就去呼叫子程式,並把呼叫結果帶回主程式繼續執行。而中斷服務程式與主程式兩者一般是無關的,不存在誰為誰服務的問題,兩者是平行關係;

第三,主程式呼叫子程式過程完全屬於軟體處理過程,不需要專門的硬體電路,而中斷處理系統是一個軟、硬體結合系統,需要專門的硬體電路才能完成中斷處理的過程;

第四,子程式巢狀可實現若干級,巢狀的最多級數由計算機記憶體開闢的堆疊大小限制,而中斷巢狀級數主要由中斷優先順序數來決定,一般優先順序數不會很大。

10、下面的程式碼輸出是什麼,為什麼?

  1. void foo(void)

  2. {

  3. unsigned int a = 6;

  4. int b = -20;

  5. (a+b > 6) ? puts("> 6") : puts("<= 6");

  6. }

解答:這個問題測試你是否懂得C語言中的整數自動轉換原則,我發現有些開發者懂得極少這些東西。不管如何,這無符號整型問題的答案是輸出是 ">6"。原因是當表示式中存在有符號型別和無符號型別時所有的運算元都自動轉換為無符號型別。因此-20變成了一個非常大的正整數,所以該表示式 計算出的結果大於6。

還有一個重要的知識點:

通常情況下,在對int型別的數值作運算時,CPU的運算速度是最快的。在x86上,32位算術運算的速度比16位算術運算的速度快一倍。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、 儘管不像非嵌入式計算機那麼常見,嵌入式系統還是有從堆(heap)中動態分配記憶體的過程的。那麼嵌入式系統中,動態分配記憶體可能發生的問題是什麼?

解答:動態分配將不可避免會產生問題:

  • 記憶體洩露:記憶體洩露通常是程式自身編碼缺陷造成,常見的 malloc記憶體後沒有free等類似的操作, 系統在執行過程當中反覆的malloc,吃掉系統記憶體,造成核心OOM,將某個程序需要申請記憶體的殺死而退出。
  • 記憶體碎片:記憶體碎片是一個系統問題,反覆的malloc和 free,而free後的記憶體又不能馬上被系統回收利用。這個是因為負責動態分配記憶體的分配演算法使得這些空閒的記憶體無法使用,這一問題的發生,原因在於這些空閒記憶體以小且不連續方式出現在不同的位置。

下面的程式碼片段的輸出是什麼,為什麼?

  1. char *ptr;

  2. if ((ptr = (char *)malloc(0)) == NULL)

  3. puts("Got a null pointer");

  4. else

  5. puts("Got a valid pointer");

函式malloc()的引數是可以時0的。

13、Typedef 在C語言中頻繁用以宣告一個已經存在的資料型別的同義字。也可以用前處理器做類似的事。例如,思考一下下面的例子:

  1. #define dPS struct s *

  2. typedef struct s * tPS;

以上兩種情況的意圖都是要定義dPS 和 tPS 作為一個指向結構s指標。哪種方法更好呢?

解答:typedef更好。思考下面的例子:

  1. dPS p1,p2;

  2. tPS p3,p4;

如果是第一個define的擴充套件:

struct s * p1, p2;

p1為指標,p2為結構體。很明顯,不是我們想要的答案。