前言
??從多個.c文件到達一個可執行文件的四步:
??預處理–>編譯–>匯編–>鏈接
預處理
??預處理過程就是預處理器處理這些預處理指令(要不然編譯器完全不認識),最終會生成 main.i的文件
主要做的事情有如下幾點:
- 展開頭文件
- 展開宏
- 條件編譯
- 刪除注釋
- 添加行號等信息
- 保留parama預處理指令
- 頭文件展開—#include指令
- #include <sdtio.h> 和 #include “stdio.h”
對于<> 搜索順序為- 通過GCC參數gcc-I指定的目錄(注:大寫的I 讓我們自由指定的)。
- 通過環境變量CINCLUDEPATH指定的目錄。
- GCC的內定目錄。
對于 " "的搜索時順序 - 項目當前目錄(此時也可以用"…/LED/led.h"方式去搜索)
- 通過GCC參數gcc-I指定的目錄。
- 通過環境變量CINCLUDEPATH指定的目錄。
- GCC的內定目錄
- 為什么把聲明放在頭文件里
- 提供一個接口 方便其他文件通過聲明調用對應的函數
- 當我們的led.c 包含了 led.h的時候 也方便編譯器做類型的檢查
- 頭文件多次包含會增加可執行文件的體積嗎?
只要是使用了類似#pragma once 或者#ifndef 多次包含是不會的增加可執行文件的體積的
同樣的要注意:聲明不會增加可執行文件的體積
- #include <sdtio.h> 和 #include “stdio.h”
- 宏展開#define 宏指令
- 宏定義最小
#define MIN(x,y) ((x) > (y) ? (y) : (x))
因為宏只是做了一個替換所以對于如下代碼
- 宏定義最小
#include <stdio.h>#define MIN(x,y) ((x) > (y) ? (y) : (x))//因為宏只是做了一個替換所以對于如下代碼int main(){int a = 2 ;int b = 5;int c = MIN(a++,b++);printf("c = %d a = %d b = %d\r\n",c,a,b); // a竟然等于4}
在這里可以用GNU C語法中的一些小技巧操作
#define MIN(x,y) ({\typeof(x) _x = (x);\typeof(y) _y = (y);\_x > _y ? _x : _y;})
- 定義一個很大的常數的時候
??#define MAX_LONG (100001000010000)UL //指定類型 - ##連接符
??高端用法 看了好多代碼都用這個 但是分析起來亂亂的,大概就是把兩個字母連接到一起
#define contact(x,y) (x##y)int bc = 50;printf("bc = %d\r\n", contact(b,c));
- offset_of與container_of
之前寫結構體的時候寫過,權當復習一下#define offset_of(type, member) ((size_t)(&((type *)0)->member)) #define container_of(type,member,ptr) (type *)((size_t) ptr - offset_of(type,member)) struct student {int height;char * name; }; int main() {struct student stu;stu.height = 50;stu.name = "123456";char ** tmp_name = &stu.name;struct student* s = &stu;printf("%p %p %ld\r\n",s, tmp_name,offset_of(struct student,height));struct student *new_s = container_of(struct student,name,tmp_name);new_s->height = 60;printf("%d\r\n",stu.height); }
``
- 宏為什么要用 do {} while(0)
??如果去看linux源碼也好還是RTOS等的代碼也好 會有很多時候用到do_while(0) 它的作用是什么呢
假設我們定義了
#define MACRO() foo(); bar()
此時我們寫了這樣的偽代碼
if (condition)
MACRO();
else
baz();
// 宏展開后和我們想要的就完全不一樣了 直接就出錯了
// do {}while(0)可以保證宏作為一個整體執行 此時就可以定義一些局部變量
- 條件編譯 #ifdef等指令
-
條件編譯指令
正常用的比較多的就是 #ifndef #define #endif這幾個連用
也有 #defined(VAR_X)之類的 -
#error指令
如果發生錯誤直接中斷編譯過程
- #pragma 指令
- #pragma pack([n]):指示結構體和聯合成員的對齊方式。
- #pragma message(“string”):在編譯信息輸出窗口打印自己的文
本信息。 - #pragma warning:有選擇地改變編譯器的警告信息行為。
- #pragma once:在頭文件中添加這條指令,可以防止頭文件多次
編譯。
編譯
??真要講編譯我也是不配講的 就我們知道這是在干嘛就行了
??編譯就是把.c文件變成匯編文件的過程
- 編譯過程的6步
詞法分析 / 語法分析 / 語義分析 / 中間代碼生成 / 匯編代碼生成 / 目標代碼生成- 語法錯誤: stynax error: 缺少分號 / {}沒擴住 /
- 語義錯誤: 類型不匹配 未定義的變量
最終的結果就是生成.S文件
- gcc的優化等級 gcc -O
可以參考
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options - 交叉編譯
嵌入式開發板一般是ARM架構 然后PC是x86架構
通過交叉編譯器進行程序的編譯
匯編
??匯編就是把匯編代碼編程機器碼 也就是比較熟悉的 xx.o文件了
??匯編過程最終會生成以零地址為鏈接起始地址的可重定位目標文件
鏈接
??把.o 文件進行組裝 需要重定位 因為所有的.o文件的開頭都是以0地址開頭的
??鏈接主要分為3個過程:分段組裝、符號決議和重定位
-
分段組裝
不太好講 基本就是通過一個腳本把多個文件按照段組合到一起
-
符號決議
符號決議的核心就行 如果說變量/函數重名了怎么辦- 不允許同時存在兩個相同的強符號
初始化的全局變量、函數名默認都是強符號,未初始化的全局變量默認是弱符號
比如
- 不允許同時存在兩個相同的強符號
// b.cint i; // 未初始化 是弱符號int main() {printf("%d\r\n",i); //i的值是20}// a.cint i = 20;
__attribute__關鍵字 可以把強符號強行轉換為弱符號 attribute((weak))
- 使用弱符號的好處
-
自定義重名函數
這里在嵌入式里最常見的就是中斷服務函數的弱定義了
當我們需要重新定義中斷服務函數的時候 只需要保證名字很start.S的名字一致就行,鏈接的時候就知道鏈接到哪里了
-
檢查該函數是否存在
// b.c
#include <stdio.h>
int global_k;
char global_i;
attribute((weak)) void func()
{
printf(“這被定義為弱符號了\r\n”);
}
int main()
{
printf(“%d\r\n”,global_k);
if(func)
func(); //調用的是強符號的函數
return 0;
}
// a.c
#include <stdio.h>
int global_k = 20;
int global_i;
void func()
{
printf(“這被定義為強符號了 fun\r\n”);
}
-
- 同樣都是弱符號 誰體積大誰勝出
- 重定位
因為要把不同的文件鏈接到一塊 所以位置就會發生變化