uc筆記01---Unix,Linux,程式構建過程,gcc,標頭檔案,預處理,環境變數配置
阿新 • • 發佈:2019-02-12
1. Unix 作業系統
1)簡介
美國 AT&T 公司貝爾實驗室,
1971 年,
肯.湯普遜、丹尼斯.裡奇。
多使用者、多工、支援多種處理器架構。
高安全性、高可靠性,高穩定性。
既可構建大型關鍵業務系統的商業伺服器應用,
也可構建面向移動終端、手持裝置等的嵌入式應用。
2)三大派生版本
a. System V
AIX: IBM,銀行
Solaris: SUN->Oracle,電信
HP-UX
IRIX
b. Berkley
FreeBSD
NetBSD
OpenBSD
Mac OS X
c. Hybrid
Minix: 迷你版的類 Unix 作業系統。
Linux: GPL,免費開源,商用伺服器(RedHat)、
桌面(Ubuntu)、嵌入式(Android)。
作業系統的基本分類:
實時,分時和批處理;
3. Linux 作業系統
1)簡介
類 Unix 作業系統,免費開源。
不同發行版本使用相同核心。
手機、平板電腦、路由器、視訊遊戲控制檯、臺式計算機、
大型計算機、超級計算機。
嚴格意義上的 Linux 僅指作業系統核心。
隸屬於 GNU 工程。
發明人 Linus Torvalds。
2)相關知識
a. Minix 作業系統
荷蘭阿姆斯特丹 Vrije 大學,
數學與計算機科學系,
Andrew S. Tanenbaum,
ACM 和 IEEE 的資深會員。
b. GNU 工程
Richard Stallman 發起於1984年,
由自由軟體基金會 (FSF) 提供支援。
GNU 的基本原則就是共享,
其主旨在於發展一個有別於一切商業 Unix 系統的,
免費且完整的類 Unix 系統——-GNU Not Unix。
3)POSIX 標準
Portable Operating System Interface for Computing Systems,
統一的系統程式設計介面規範。
由 IEEE 和 ISO/IEC 開發。
保證應用程式原始碼級的可移植性。
Linux 完全遵循 POSIX 標準。
4)GPL 通用公共許可證
允許對某成果及其派生成果的重用、修改和複製,
對所有人都是自由的,但不能宣告做了原始工作,或宣告由他人所做。
5)版本
早期版本:0.01, 0.02, . . . , 0.99, 1.0
舊計劃:介於 1.0 和 2.6 之間,A.B.C
A: 主版本號,核心大幅更新。
B: 次版本號,核心重大修改,奇數測試版,偶數穩定版。
C: 補丁序號,核心輕微修訂。
2003 年 12 月釋出 2.6.0 以後:縮短髮布週期,A.B.C-D.E
D: 構建次數,反映極微小的更新。
E: 描述資訊。
rc/r - 候選版本,其後的數字表示第幾個候選版本,
越大越接近正式版本
smp - 對稱多處理器
pp - Red Hat Linux 的測試版本
EL - Red Hat Linux 的企業版本
mm - 測試新技術或新功能
fc - Red Hat Linux 的 Fedora Core 版本
用 cat /proc/version 命令檢視系統版本資訊:
# cat /proc/version
Linux version 3.6.11-4.fc16.i686
# cat /proc/version
Linux version 3.2.0-39-generic-pae
6)特點
遵循 GNU/GPL
開放性
多使用者
多工
裝置獨立性
豐富的網路功能
可靠的系統安全
良好的可移植性
7)發行版本
大眾的 Ubuntu
優雅的 Linux Mint
銳意的 Fedora
華麗的 openSUSE
自由的 Debian
簡潔的 Slackware
老牌的 RedHat
4. 構建過程 Build
編輯 -> 預編譯 -> 編譯 -> 彙編 -> 連結
1) 編輯: vi hello.c -> hello.c
2) 預編譯:gcc -E hello.c -o hello.i -> hello.i ---+
3) 編譯: gcc -S hello.i -> hello.s | GCC
4) 彙編: gcc -c hello.s -> hello.o | 工具鏈
5) 連結: gcc hello.o -o hello -> hello ---+
5. GNU 編譯工具 GCC
支援多種程式語言
C、C++、Objective-C、Java、Fortran、Pascal、Ada
支援多種平臺
Unix、Linux、Windows
編譯多個源程式
gcc [選項引數] 檔案 1 檔案 2 . . .
gcc [選項引數] 檔案
-c - 只編譯不連結
-o - 指定輸出檔案
-E - 預編譯
-S - 產生彙編檔案
-pedantic - 對不符合ANSI/ISO C語言標準的
擴充套件語法產生警告
-Wall - 產生儘可能多的警告。 // Waring all
範例:gcc -Wall wall.c
-Werror - 將警告作為錯誤處理。 // Waring error
範例:gcc -Werror werror.c
-x - 指定原始碼的語言。
範例:gcc -x c++ cpp.c -lstdc++
-g - 生成除錯資訊
-O1/O2/O3 - 優化等級 // O1 最低,O3 最高;注意:是英文 o,不是零;
檔案字尾
.h - C 語言原始碼標頭檔案
.c - 預處理前的 C 語言原始碼檔案
.i - 預處理後的 C 語言原始碼檔案
.s - 組合語言檔案
.o - 目標檔案
.a - 靜態庫檔案(後面講)
.so - 共享庫(動態庫)檔案(後面講)
可以用 nm 命令產看目標檔案:
nm hello.o
000000 T mian // T 程式碼區
U put // U 標準庫
6. 標頭檔案
思考:標頭檔案的作用是什麼?
1) 宣告外部變數、函式和類。
2) 定義巨集、類型別名和自定義型別。
3) 包含其它標頭檔案。
4) 藉助標頭檔案衛士,防止因同一個標頭檔案被多次包含,而引發重定義錯。
包含標頭檔案時需要注意:
1)#include <...>
先找 -I 指定的目錄,再找系統目錄。
2)#include "..."
先找 -I 指定的目錄,再找當前目錄,最後找系統目錄。
3)gcc 的 -I 選項
指定標頭檔案附加搜尋路徑。
4)標頭檔案的系統目錄
/usr/include
/usr/local/include
/usr/lib/gcc/i686-linux-gnu/4.6.3/include
/usr/include/c++/4.6.3 (C++ 編譯器優先查詢此目錄)
標頭檔案的三種定位方式:
1) #include "目錄/xxx.h" - 標頭檔案路徑發生變化,需要修改源程式;
2) C_INCLUDE_PATH/CPATH=目錄 - 同時構建多個工程,可能引發衝突;(在 gcc 裡設定,詳見後面環境變數)
3) gcc -I目錄 - 既不用改程式,也不會有衝突
7. 預處理
預處理又稱為預編譯,是做些程式碼文字的替換工作。
處理 # 開頭的指令,比如拷貝 #include 包含的檔案程式碼,
#define 巨集定義的替換,條件編譯等,就是為編譯做的預備工作的階段,
主要處理 # 開始的預編譯指令,預編譯指令指示了在程式正式編譯前就由編譯器進行的操作,可以放在程式中的任何位置。
c 編譯系統在對程式進行通常的編譯之前,先進行預處理。
c 提供的預處理功能主要有以下三種:
1)巨集定義
2)檔案包含
3)條件編譯
以下情況需要使用預處理:
1)總是使用不經常改動的大型程式碼體。
2)程式由多個模組組成,所有模組都使用一組標準的包含檔案和相同的編譯選項。
在這種情況下,可以將所有包含檔案預編譯為一個預編譯頭。
常用的預處理指令:
#include // 將指定檔案的內容插至此指令處
#include_next // 與 #include 一樣,但從當前目錄之後的目錄查詢,極少用
#define // 定義巨集
#undef // 刪除巨集
#if // 判定
#ifdef // 判定巨集是否已定義
#ifndef // 判定巨集是否未定義
#else // 與 #if、#ifdef、#ifndef 結合使用
#elif // else if 多選分支
#endif // 結束判定
## // 連線巨集內兩個連續的字串
# // 將巨集引數擴充套件成字串字面值
#error // 產生錯誤,結束預處理
#warning // 產生警告
8. 環境變數(參見後面的庫)
C_INCLUDE_PATH - C 標頭檔案的附加搜尋路徑,相當於 gcc 的 -I 選項
CPATH - 同 C_INCLUDE_PATH
CPLUS_INCLUDE_PATH - C++ 標頭檔案的附加搜尋路徑
LIBRARY_PATH - 連結時查詢靜態庫/共享庫的路徑
LD_LIBRARY_PATH - 執行時查詢共享庫的路徑
舉例:
標頭檔案:cal.h
#ifndef _CALC_H
#define _CALC_H
double add (double, double);
#endif
定義:add.c
double add (double a, double b) {
return a + b;
}
主函式:main.c
#include <stdio.h>
#include <cal.h>
// #include "cal.h"
int main () {
printf ("%lf\n", add (10, 20));
return 0;
}
這裡 #include <cal.h> 預設到系統目錄下找 cal.h 這個檔案,會編譯報錯;
解決這個問題就需要自己定義搜尋路徑:
方法 1:通過 gcc 的 -I 選項指定 C/C++ 標頭檔案的附加搜尋路徑:
#gcc add.c main.c -I. // 在當前目錄搜尋
方法 2:通過環境變數 CPATH 增加編譯器搜尋路徑,這種方法只在當前 shell 有效;
#export CPATH=$CPATH:.
#echo $CPATH // 檢視當前環境變數
方法 3:也可以在 ~/.bashrc 或 ~/.bash_profile,配置檔案中寫環境變數,持久有效:
#ls -a
#vi .bashrc // 進入 .bashrc
或者:
#vi .bash_profile
在 bashrc / bash_profile 裡輸入下面語句:
export CPATH=$CPATH:.
退出 bashrc,執行下面語句:
#source ~/.bashrc
或者:
#source ~/.bash_profile
檢視結果:
#echo $CPATH
或者:
#env | grep CPATH
取消環境變數設定:
# unset CPATH
# unset LIBRARY_PATH // 取消靜態庫和動態庫的環境變數
9. VERSION 版本控制
檔案:error.c
#include <stdio.h>
#if (VERSION < 2)
#error "版本太低"
#elif (VERSION > 3)
#warning "版本太高"
#endif
int main () {
printf("%d\n", VERSION);
return 0;
}
測試結果如下:
輸入:
# gcc error.c -DVERSION=2
輸出:
error.c:4:3: error: #error "Version too low !"
輸入:
# gcc error.c -DVERSION=3
輸出:
(空行)
輸入:
# gcc error.c -DVERSION=4
輸出:
error.c:6:3: warning: #warning "Version too high !" [-Wcpp]
10 #line 指定行號:
int main () {
printf ("%d\n", __LINE__); // 輸出當前行號
#line 100
printf ("%d\n", __LINE__); // 輸出:100
printf ("%d\n", __LINE__); // 輸出:101
return 0;
}
11 #pragma 提供額外資訊的標準方法,
設定編譯器的狀態或者指示編譯器完成一些特定的動作;
#pragma GCC dependency <檔案> // 若 <檔案> 比當前檔案新,則產生警告
#pragma GCC poison <標識> // 若出現 <標識>,則產生錯誤
#pragma pack(1/2/4/8) // 按 1/2/4/8 位元組,對齊補齊
例 1:pragma.c
#include <stdio.h>
#pragma GCC dependency "dep.c"
// 如果 dep.c 比當前檔案 pragma.c 新,則發出警告;
#pragma GCC poison goto int
// 如果當前程式中出現 goto/int 關鍵字,則發出警告;
int main () {
goto escape;
return 0;
escape:
printf ("hello");
return 0;
}
tips:這裡有 goto 語句,所以執行到第一個 return 0; 的時候不會跳出程式,
而是跳過 return,執行 escape 後面的語句;
例 2:
#include <stdio.h>
int main () {
#pragma pack(1) // 按 1 位元組對齊補齊
struct S1 {
double d;
int i;
char c;
short h;
};
// DDDDDDDDIIIICHH
printf ("%u\n", sizeof (struct S1); // 輸出:15
#pragma pack() // 按預設方式對齊,32 位系統為 4 位元組,64 位系統為 8 位元組
struct S2 {
double d;
int i;
char c;
short h;
};
// DDDD DDDD IIII CXSS
printf ("%u\n", sizeof (struct S2); // 輸出:16
#pragma pack(8) // 按 8 位元組對齊,如果是 32 位系統,則只能還是按 4 位元組對齊;
struct S3 {
double d;
int i;
char c;
short h;
};
printf ("%u\n", sizeof (struct S3); // 輸出:16
return 0;
}
12. 預定義巨集
例 1:
檔案:print.h
#ifndef _PRINT_H
#define _PRINT_H
#include <stdio.h>
void print () {
printf ("%s\n", __BASE_FILE__); // 正在編譯的檔案:predef.c
printf ("%s\n", __FILE__); // 所在檔案:print.h
printf ("%d\n", __LINE__);
printf ("%s\n", __FUNCTION__);
printf ("%s\n", __func__);
printf ("%s\n", __DATE__);
printf ("%s\n", __TIME__);
printf ("%d\n", __INCLUDE_LEVEL__); // 包含 include 層數:2
#ifdef __cplusplus // 判斷是否是由 c++ 程式編譯;
printf ("%d\n", __cplusplus); // 當用 c++ 編譯此檔案時,會返回 1;否則什麼都不返回;
#endif // __cplusplus
}
#endif // _PRINT_H
-----------------------
檔案:predef.h
#ifndef _PREDEF_H
#define _PREDEF_H
#include "print.h"
#endif // _PREDEF_H
-----------------------
檔案:predef.c
#include "predef.h"
int main () {
print ();
return 0;
}
13. 靜態庫
連結靜態庫是將庫中的被呼叫程式碼複製到呼叫模組中,
而連結共享庫則只是在呼叫模組中,
嵌入被呼叫程式碼在庫中的(相對)地址。
靜態庫佔用空間非常大,不易修改但執行效率高。
共享庫佔用空間小,易於修改但執行效率略低。
靜態庫的預設副檔名是 .a,共享庫的預設副檔名是 .so。
編寫一個演算法程式,其中演算法位於 cal.c 檔案裡,列印位於 show.c 檔案裡;
各自都包含有各自的標頭檔案,然後把整個檔案打包給客戶;
客戶編寫 main.c 呼叫演算法程式;
步驟:
1)編寫 cal.h 和 cal.c;執行生成 cal.o 目標檔案;
2)編寫 show.h 和 show.c;執行生成 show.o 目標檔案;
3)編寫 math.h,用於包含 cal.h 和 show.h,這樣客戶就不用載入多個頭檔案了;
4)把 cal.o 和 show.o 打包成靜態庫 libmath.a;
5)把所有標頭檔案和靜態庫給客戶;(必須是所有標頭檔案都給客戶)
6)客戶編寫測試程式,僅需要載入 math.h 標頭檔案就可以呼叫庫中函式;
編寫靜態庫
=====
標頭檔案:cal.h
#ifndef _CALC_H
#define _CALC_H
int add (int a, int b);
int sub (int a, int b);
#endif // _CALC_H
檔案:calc.c
#include "calc.h"
int add (int a, int b) {
return a + b;
}
int sub (int a, int b) {
return a - b;
}
生成目標檔案:
gcc -c calc.c ---> calc.o
標頭檔案:show.h
#ifndef _SHOW_H
#define _SHOW_H
void show (int a, char op, int b, int c);
#endif // _SHOW_H
檔案:show.c
#include <stdio.h>
#include "show.h"
void show (int a, char op, int b, int c) {
printf ("%d%c%d=%d\n", a, op, b, c);
}
生成目標檔案:
gcc -c show.c ---> show.o
標頭檔案:math.h
#ifndef _MATH_H
#define _MATH_H
#include "calc.h"
#include "show.h"
#endif // _MATH_H
壓縮成靜態庫
======
ar -r libmath.a calc.o show.o ---> libmath.a
nm libmath.a // 檢視靜態庫
ar指令:ar [選項] 靜態庫檔名 目標檔案列表
-r - 將目標檔案插入到靜態庫中,已存在則更新
-q - 將目標檔案追加到靜態庫尾
-d - 從靜態庫中刪除目標檔案
-t - 列表顯示靜態庫中的目標檔案
-x - 將靜態庫展開為目標檔案
注意:提供靜態庫的同時也需要提供標頭檔案。
呼叫靜態庫(客戶測試)
==========
檔案:main.c
#include "math.h"
int main () {
show (30, '+', 23, add (30, 23));
return 0;
}
方法 1:直接編譯:
gcc main.c libmath.a
./a.out
方法 2:通過 -L 手動指定靜態庫連線地址:
gcc main.c -lmath -L. // -l 會自動加上 lib 和 .a;
./a.out
方法 3:設定臨時環境變數:
export LIBRAR_PATH=$LIBRART_PATH:. // gcc 連結時查詢靜態庫/共享庫的路徑
gcc main.c -lmath
./a.out
方法 4:也可以在 ~/.bashrc 或 ~/.bash_profile,配置檔案中寫環境變數,持久有效:
#ls -a
#vi .bashrc // 進入 .bashrc
或者:#vi .bash_profile
在 bashrc 裡輸入下面語句:
export CPATH=$CPATH:.
退出 bashrc,執行下面語句:
#source ~/.bashrc
或者:#source ~/.bash_profile
檢視結果:
#echo $CPATH
執行靜態庫
=====
# ./a.out
在可執行程式的連結階段,已將所呼叫的函式的二進位制程式碼,
複製到可執行程式中,因此執行時不需要依賴靜態庫。
14. 動態庫
1)建立動態庫:
=======
把上面所有的標頭檔案和目標檔案都拷貝一份,作如下操作:
gcc -c -fpic calc.c --> calc.o
gcc -c -fpic show.c --> show.o // 編譯器
gcc -shared calc.o show.o -o libmath.so --> libmath.so // 連結器
或一次完成編譯和連結:
# gcc -shared -fpic calc.c show.c -o libmath.so
PIC (Position Independent Code):位置無關程式碼。
可執行程式載入它們時,可將其對映到其地址空間的任何位置。
-fPIC : 大模式,生成程式碼比較大,執行速度比較慢,所有平臺都支援。
-fpic : 小模式,生成程式碼比較小,執行速度比較快,僅部分平臺支援。
注意:提供共享庫的同時也需要提供標頭檔案。
2)呼叫動態庫
=======
方法 1:
# gcc main.c libmath.so (直接法)
方法 2:
通過 LIBRARY_PATH 環境變數指定庫路徑:
# export LIBRARY_PATH=$LIBRARY_PATH:.
# gcc main.c -lmath (環境法)
方法 3:
通過 gcc 的 -L 選項指定庫路徑:
# unset LIBRARY_PATH // 取消方法 2 中設定的環境變數
# gcc main.c -lmath -L. (引數法)
方法 4:也可以在 ~/.bashrc 或 ~/.bash_profile,配置檔案中寫環境變數,
方法同上;
方法 5:
一般化的方法:
# gcc .c/.o -l<庫名> -L<庫路徑>
3)執行動態庫
=======
執行時需要保證 LD_LIBRARY_PATH 環境變數中包含共享庫所在的路徑:
# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
# ./a.out
在可執行程式的連結階段,並不將所呼叫函式的二進位制程式碼複製到可執行程式中,
而只是將該函式在共享庫中的地址嵌入到可執行程式中,因此執行時需要依賴共享庫。
15. 動態載入共享庫/動態庫
#include <dlfcn.h>
1)載入共享庫
void* dlopen (const char* filename, int flag);
成功返回共享庫控制代碼,失敗返回 NULL;
filename 為共享庫路徑,若只給檔名,則根據 LD_LIBRARY_PATH 搜尋;
flag取值:
RTLD_LAZY - 延遲載入,使用共享庫中的符號(如呼叫函式)時才載入。
RTLD_NOW - 立即載入。
2)獲取函式地址
void* dlsym (void* handle, const char* symbol);
成功返回函式地址,失敗返回 NULL;
handle 為共享庫控制代碼,symbol 為函式名;
3)解除安裝共享庫
int dlclose (void* handle);
成功返回 0,失敗返回非零。
4)獲取錯誤資訊
char* dlerror (void);
有錯誤發生則返回錯誤資訊字串指標,否則返回 NULL。
範例:load.c
#include <stdio.h>
#include <dlfcn.h>
typedef int (*PFUN_CALC) (int, int); // 定義函式指標型別
typedef void (*PFUNC_SHOW) (int, char, int, int);
int main () {
// 建立控制代碼
void* handle = dlopen ("shared/libmath.so", RTLD_NOW);
if (! handle) {
fprintf (stderr, "dlopen:%s\n", dlerror ());
return -1;
}
// 取出函式
PFUNC_CALC add = (PFUNC_CALC)dlsym (handle, "add");
if (! add) {
fprintf (stderr, "dlsym:%s\n", dlerror ());
return -1;
}
PFUNC_CALC sub = (PFUNC_CALC)dlsym (handle, "sub");
if (! sub) {
fprintf (stderr, "dlsym:%s\n", dlerror ());
return -1;
}
PFUNC_SHOW show = (PFUNC_CALC)dlsym (handle, "show");
if (! show) {
fprintf (stderr, "dlsym:%s\n", dlerror ());
return -1;
}
// 呼叫
show (30, '+', 20, add (30, 20));
show (30, '+', 20, sub (30, 20));
// 關閉
if (dlclose (handle)) {
fprintf (stderr, "dlclose:%s\n", dlerror ());
return -1;
}
return 0;
}
# gcc load.c -ldl
注意:連結時不再需要 -lmath,但需要 -ldl(連線動態庫)。
16. 輔助工具
nm: 檢視目標檔案、可執行檔案、靜態庫、共享庫中的符號列表。
ldd: 檢視可執行檔案和共享庫的動態依賴。
ldconfig: 共享庫管理。
事先將共享庫的路徑資訊寫入 /etc/ld.so.conf 配置檔案中,
ldconfig 根據該配置檔案生成 /etc/ld.so.cache 緩衝檔案,
並將該緩衝檔案載入記憶體,藉以提高共享庫的載入效率。
系統啟動時自動執行 ldconfig,但若修改了共享庫配置,則需要手動執行該程式。
strip: 減肥。去除目標檔案、可執行檔案、靜態庫和共享庫中的符號列表、除錯資訊等。
objdump: 顯示二進位制模組的反彙編資訊。
# objdump -S a.out
指令地址 機器指令 彙編指令
-------- -------------------- ---------------------
8048514: 55 push %ebp
8048515: 89 e5 mov %esp,%ebp
8048517: 83 e4 f0 and $0xfffffff0,%esp
804851a: 83 ec 20 sub $0x20,%esp
804851d: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
17. 作業:編寫一個函式 diamond(),列印一個菱形,其高度、
寬度、實心或者空心以及圖案字元,均可通過引數設定。
分別封裝為靜態庫 libdiamond_static.a 和動態庫 libdiamond_shared.so,並呼叫之。
標頭檔案:diamond.h
#ifndef _DIAMOND_H
#define _DIAMOND_H
void diamond (int h, int d, int s, char c);
#endif // _DIAMOND_H
檔案:diamond.c
#include <stdio.h>
#include "diamond.h"
void diamond (int h, int d, int s, char c) {
int i, j;
for (i = 0; i <= h; i++) {
printf ("%*s", (h - i) * d, "");
for (j = 0; j < i * d * 2; j++)
printf ("%c", j ? (s ? c : ' ') : c);
printf ("%c\n", c);
}
for (i = 1; i <= h; i++) {
printf ("%*s", i * d, "");
for (j = 0; j < (h - i) * d * 2; j++)
printf ("%c", j ? (s ? c : ' ') : c);
printf ("%c\n", c);
}
}
主函式:main.c
#include <stdio.h>
#include "diamond.h"
int main (int argc, char* argv[]) {
if (argc < 5) {
fprintf (stderr, "Usage: %s <H> <D> <S> <C>\n", argv[0]);
return -1;
}
diamond (atoi (argv[1]), atoi (argv[2]), atoi (argv[3]), *argv[4]);
return 0;
}
1)簡介
美國 AT&T 公司貝爾實驗室,
1971 年,
肯.湯普遜、丹尼斯.裡奇。
多使用者、多工、支援多種處理器架構。
高安全性、高可靠性,高穩定性。
既可構建大型關鍵業務系統的商業伺服器應用,
也可構建面向移動終端、手持裝置等的嵌入式應用。
2)三大派生版本
a. System V
AIX: IBM,銀行
Solaris: SUN->Oracle,電信
HP-UX
IRIX
b. Berkley
FreeBSD
NetBSD
OpenBSD
Mac OS X
c. Hybrid
Minix: 迷你版的類 Unix 作業系統。
Linux: GPL,免費開源,商用伺服器(RedHat)、
桌面(Ubuntu)、嵌入式(Android)。
作業系統的基本分類:
實時,分時和批處理;
3. Linux 作業系統
1)簡介
類 Unix 作業系統,免費開源。
不同發行版本使用相同核心。
手機、平板電腦、路由器、視訊遊戲控制檯、臺式計算機、
大型計算機、超級計算機。
嚴格意義上的 Linux 僅指作業系統核心。
隸屬於 GNU 工程。
發明人 Linus Torvalds。
2)相關知識
a. Minix 作業系統
荷蘭阿姆斯特丹 Vrije 大學,
數學與計算機科學系,
Andrew S. Tanenbaum,
ACM 和 IEEE 的資深會員。
b. GNU 工程
Richard Stallman 發起於1984年,
由自由軟體基金會 (FSF) 提供支援。
GNU 的基本原則就是共享,
其主旨在於發展一個有別於一切商業 Unix 系統的,
免費且完整的類 Unix 系統——-GNU Not Unix。
3)POSIX 標準
Portable Operating System Interface for Computing Systems,
統一的系統程式設計介面規範。
由 IEEE 和 ISO/IEC 開發。
保證應用程式原始碼級的可移植性。
Linux 完全遵循 POSIX 標準。
4)GPL 通用公共許可證
允許對某成果及其派生成果的重用、修改和複製,
對所有人都是自由的,但不能宣告做了原始工作,或宣告由他人所做。
5)版本
早期版本:0.01, 0.02, . . . , 0.99, 1.0
舊計劃:介於 1.0 和 2.6 之間,A.B.C
A: 主版本號,核心大幅更新。
B: 次版本號,核心重大修改,奇數測試版,偶數穩定版。
C: 補丁序號,核心輕微修訂。
2003 年 12 月釋出 2.6.0 以後:縮短髮布週期,A.B.C-D.E
D: 構建次數,反映極微小的更新。
E: 描述資訊。
rc/r - 候選版本,其後的數字表示第幾個候選版本,
越大越接近正式版本
smp - 對稱多處理器
pp - Red Hat Linux 的測試版本
EL - Red Hat Linux 的企業版本
mm - 測試新技術或新功能
fc - Red Hat Linux 的 Fedora Core 版本
用 cat /proc/version 命令檢視系統版本資訊:
# cat /proc/version
Linux version 3.6.11-4.fc16.i686
# cat /proc/version
Linux version 3.2.0-39-generic-pae
6)特點
遵循 GNU/GPL
開放性
多使用者
多工
裝置獨立性
豐富的網路功能
可靠的系統安全
良好的可移植性
7)發行版本
大眾的 Ubuntu
優雅的 Linux Mint
銳意的 Fedora
華麗的 openSUSE
自由的 Debian
簡潔的 Slackware
老牌的 RedHat
4. 構建過程 Build
編輯 -> 預編譯 -> 編譯 -> 彙編 -> 連結
1) 編輯: vi hello.c -> hello.c
2) 預編譯:gcc -E hello.c -o hello.i -> hello.i ---+
3) 編譯: gcc -S hello.i -> hello.s | GCC
4) 彙編: gcc -c hello.s -> hello.o | 工具鏈
5) 連結: gcc hello.o -o hello -> hello ---+
5. GNU 編譯工具 GCC
支援多種程式語言
C、C++、Objective-C、Java、Fortran、Pascal、Ada
支援多種平臺
Unix、Linux、Windows
編譯多個源程式
gcc [選項引數] 檔案 1 檔案 2 . . .
gcc [選項引數] 檔案
-c - 只編譯不連結
-o - 指定輸出檔案
-E - 預編譯
-S - 產生彙編檔案
-pedantic - 對不符合ANSI/ISO C語言標準的
擴充套件語法產生警告
-Wall - 產生儘可能多的警告。 // Waring all
範例:gcc -Wall wall.c
-Werror - 將警告作為錯誤處理。 // Waring error
範例:gcc -Werror werror.c
-x - 指定原始碼的語言。
範例:gcc -x c++ cpp.c -lstdc++
-g - 生成除錯資訊
-O1/O2/O3 - 優化等級 // O1 最低,O3 最高;注意:是英文 o,不是零;
檔案字尾
.h - C 語言原始碼標頭檔案
.c - 預處理前的 C 語言原始碼檔案
.i - 預處理後的 C 語言原始碼檔案
.s - 組合語言檔案
.o - 目標檔案
.a - 靜態庫檔案(後面講)
.so - 共享庫(動態庫)檔案(後面講)
可以用 nm 命令產看目標檔案:
nm hello.o
000000 T mian // T 程式碼區
U put // U 標準庫
6. 標頭檔案
思考:標頭檔案的作用是什麼?
1) 宣告外部變數、函式和類。
2) 定義巨集、類型別名和自定義型別。
3) 包含其它標頭檔案。
4) 藉助標頭檔案衛士,防止因同一個標頭檔案被多次包含,而引發重定義錯。
包含標頭檔案時需要注意:
1)#include <...>
先找 -I 指定的目錄,再找系統目錄。
2)#include "..."
先找 -I 指定的目錄,再找當前目錄,最後找系統目錄。
3)gcc 的 -I 選項
指定標頭檔案附加搜尋路徑。
4)標頭檔案的系統目錄
/usr/include
/usr/local/include
/usr/lib/gcc/i686-linux-gnu/4.6.3/include
/usr/include/c++/4.6.3 (C++ 編譯器優先查詢此目錄)
標頭檔案的三種定位方式:
1) #include "目錄/xxx.h" - 標頭檔案路徑發生變化,需要修改源程式;
2) C_INCLUDE_PATH/CPATH=目錄 - 同時構建多個工程,可能引發衝突;(在 gcc 裡設定,詳見後面環境變數)
3) gcc -I目錄 - 既不用改程式,也不會有衝突
7. 預處理
預處理又稱為預編譯,是做些程式碼文字的替換工作。
處理 # 開頭的指令,比如拷貝 #include 包含的檔案程式碼,
#define 巨集定義的替換,條件編譯等,就是為編譯做的預備工作的階段,
主要處理 # 開始的預編譯指令,預編譯指令指示了在程式正式編譯前就由編譯器進行的操作,可以放在程式中的任何位置。
c 編譯系統在對程式進行通常的編譯之前,先進行預處理。
c 提供的預處理功能主要有以下三種:
1)巨集定義
2)檔案包含
3)條件編譯
以下情況需要使用預處理:
1)總是使用不經常改動的大型程式碼體。
2)程式由多個模組組成,所有模組都使用一組標準的包含檔案和相同的編譯選項。
在這種情況下,可以將所有包含檔案預編譯為一個預編譯頭。
常用的預處理指令:
#include // 將指定檔案的內容插至此指令處
#include_next // 與 #include 一樣,但從當前目錄之後的目錄查詢,極少用
#define // 定義巨集
#undef // 刪除巨集
#if // 判定
#ifdef // 判定巨集是否已定義
#ifndef // 判定巨集是否未定義
#else // 與 #if、#ifdef、#ifndef 結合使用
#elif // else if 多選分支
#endif // 結束判定
## // 連線巨集內兩個連續的字串
# // 將巨集引數擴充套件成字串字面值
#error // 產生錯誤,結束預處理
#warning // 產生警告
8. 環境變數(參見後面的庫)
C_INCLUDE_PATH - C 標頭檔案的附加搜尋路徑,相當於 gcc 的 -I 選項
CPATH - 同 C_INCLUDE_PATH
CPLUS_INCLUDE_PATH - C++ 標頭檔案的附加搜尋路徑
LIBRARY_PATH - 連結時查詢靜態庫/共享庫的路徑
LD_LIBRARY_PATH - 執行時查詢共享庫的路徑
舉例:
標頭檔案:cal.h
#ifndef _CALC_H
#define _CALC_H
double add (double, double);
#endif
定義:add.c
double add (double a, double b) {
return a + b;
}
主函式:main.c
#include <stdio.h>
#include <cal.h>
// #include "cal.h"
int main () {
printf ("%lf\n", add (10, 20));
return 0;
}
這裡 #include <cal.h> 預設到系統目錄下找 cal.h 這個檔案,會編譯報錯;
解決這個問題就需要自己定義搜尋路徑:
方法 1:通過 gcc 的 -I 選項指定 C/C++ 標頭檔案的附加搜尋路徑:
#gcc add.c main.c -I. // 在當前目錄搜尋
方法 2:通過環境變數 CPATH 增加編譯器搜尋路徑,這種方法只在當前 shell 有效;
#export CPATH=$CPATH:.
#echo $CPATH // 檢視當前環境變數
方法 3:也可以在 ~/.bashrc 或 ~/.bash_profile,配置檔案中寫環境變數,持久有效:
#ls -a
#vi .bashrc // 進入 .bashrc
或者:
#vi .bash_profile
在 bashrc / bash_profile 裡輸入下面語句:
export CPATH=$CPATH:.
退出 bashrc,執行下面語句:
#source ~/.bashrc
或者:
#source ~/.bash_profile
檢視結果:
#echo $CPATH
或者:
#env | grep CPATH
取消環境變數設定:
# unset CPATH
# unset LIBRARY_PATH // 取消靜態庫和動態庫的環境變數
9. VERSION 版本控制
檔案:error.c
#include <stdio.h>
#if (VERSION < 2)
#error "版本太低"
#elif (VERSION > 3)
#warning "版本太高"
#endif
int main () {
printf("%d\n", VERSION);
return 0;
}
測試結果如下:
輸入:
# gcc error.c -DVERSION=2
輸出:
error.c:4:3: error: #error "Version too low !"
輸入:
# gcc error.c -DVERSION=3
輸出:
(空行)
輸入:
# gcc error.c -DVERSION=4
輸出:
error.c:6:3: warning: #warning "Version too high !" [-Wcpp]
10 #line 指定行號:
int main () {
printf ("%d\n", __LINE__); // 輸出當前行號
#line 100
printf ("%d\n", __LINE__); // 輸出:100
printf ("%d\n", __LINE__); // 輸出:101
return 0;
}
11 #pragma 提供額外資訊的標準方法,
設定編譯器的狀態或者指示編譯器完成一些特定的動作;
#pragma GCC dependency <檔案> // 若 <檔案> 比當前檔案新,則產生警告
#pragma GCC poison <標識> // 若出現 <標識>,則產生錯誤
#pragma pack(1/2/4/8) // 按 1/2/4/8 位元組,對齊補齊
例 1:pragma.c
#include <stdio.h>
#pragma GCC dependency "dep.c"
// 如果 dep.c 比當前檔案 pragma.c 新,則發出警告;
#pragma GCC poison goto int
// 如果當前程式中出現 goto/int 關鍵字,則發出警告;
int main () {
goto escape;
return 0;
escape:
printf ("hello");
return 0;
}
tips:這裡有 goto 語句,所以執行到第一個 return 0; 的時候不會跳出程式,
而是跳過 return,執行 escape 後面的語句;
例 2:
#include <stdio.h>
int main () {
#pragma pack(1) // 按 1 位元組對齊補齊
struct S1 {
double d;
int i;
char c;
short h;
};
// DDDDDDDDIIIICHH
printf ("%u\n", sizeof (struct S1); // 輸出:15
#pragma pack() // 按預設方式對齊,32 位系統為 4 位元組,64 位系統為 8 位元組
struct S2 {
double d;
int i;
char c;
short h;
};
// DDDD DDDD IIII CXSS
printf ("%u\n", sizeof (struct S2); // 輸出:16
#pragma pack(8) // 按 8 位元組對齊,如果是 32 位系統,則只能還是按 4 位元組對齊;
struct S3 {
double d;
int i;
char c;
short h;
};
printf ("%u\n", sizeof (struct S3); // 輸出:16
return 0;
}
12. 預定義巨集
例 1:
檔案:print.h
#ifndef _PRINT_H
#define _PRINT_H
#include <stdio.h>
void print () {
printf ("%s\n", __BASE_FILE__); // 正在編譯的檔案:predef.c
printf ("%s\n", __FILE__); // 所在檔案:print.h
printf ("%d\n", __LINE__);
printf ("%s\n", __FUNCTION__);
printf ("%s\n", __func__);
printf ("%s\n", __DATE__);
printf ("%s\n", __TIME__);
printf ("%d\n", __INCLUDE_LEVEL__); // 包含 include 層數:2
#ifdef __cplusplus // 判斷是否是由 c++ 程式編譯;
printf ("%d\n", __cplusplus); // 當用 c++ 編譯此檔案時,會返回 1;否則什麼都不返回;
#endif // __cplusplus
}
#endif // _PRINT_H
-----------------------
檔案:predef.h
#ifndef _PREDEF_H
#define _PREDEF_H
#include "print.h"
#endif // _PREDEF_H
-----------------------
檔案:predef.c
#include "predef.h"
int main () {
print ();
return 0;
}
13. 靜態庫
連結靜態庫是將庫中的被呼叫程式碼複製到呼叫模組中,
而連結共享庫則只是在呼叫模組中,
嵌入被呼叫程式碼在庫中的(相對)地址。
靜態庫佔用空間非常大,不易修改但執行效率高。
共享庫佔用空間小,易於修改但執行效率略低。
靜態庫的預設副檔名是 .a,共享庫的預設副檔名是 .so。
編寫一個演算法程式,其中演算法位於 cal.c 檔案裡,列印位於 show.c 檔案裡;
各自都包含有各自的標頭檔案,然後把整個檔案打包給客戶;
客戶編寫 main.c 呼叫演算法程式;
步驟:
1)編寫 cal.h 和 cal.c;執行生成 cal.o 目標檔案;
2)編寫 show.h 和 show.c;執行生成 show.o 目標檔案;
3)編寫 math.h,用於包含 cal.h 和 show.h,這樣客戶就不用載入多個頭檔案了;
4)把 cal.o 和 show.o 打包成靜態庫 libmath.a;
5)把所有標頭檔案和靜態庫給客戶;(必須是所有標頭檔案都給客戶)
6)客戶編寫測試程式,僅需要載入 math.h 標頭檔案就可以呼叫庫中函式;
編寫靜態庫
=====
標頭檔案:cal.h
#ifndef _CALC_H
#define _CALC_H
int add (int a, int b);
int sub (int a, int b);
#endif // _CALC_H
檔案:calc.c
#include "calc.h"
int add (int a, int b) {
return a + b;
}
int sub (int a, int b) {
return a - b;
}
生成目標檔案:
gcc -c calc.c ---> calc.o
標頭檔案:show.h
#ifndef _SHOW_H
#define _SHOW_H
void show (int a, char op, int b, int c);
#endif // _SHOW_H
檔案:show.c
#include <stdio.h>
#include "show.h"
void show (int a, char op, int b, int c) {
printf ("%d%c%d=%d\n", a, op, b, c);
}
生成目標檔案:
gcc -c show.c ---> show.o
標頭檔案:math.h
#ifndef _MATH_H
#define _MATH_H
#include "calc.h"
#include "show.h"
#endif // _MATH_H
壓縮成靜態庫
======
ar -r libmath.a calc.o show.o ---> libmath.a
nm libmath.a // 檢視靜態庫
ar指令:ar [選項] 靜態庫檔名 目標檔案列表
-r - 將目標檔案插入到靜態庫中,已存在則更新
-q - 將目標檔案追加到靜態庫尾
-d - 從靜態庫中刪除目標檔案
-t - 列表顯示靜態庫中的目標檔案
-x - 將靜態庫展開為目標檔案
注意:提供靜態庫的同時也需要提供標頭檔案。
呼叫靜態庫(客戶測試)
==========
檔案:main.c
#include "math.h"
int main () {
show (30, '+', 23, add (30, 23));
return 0;
}
方法 1:直接編譯:
gcc main.c libmath.a
./a.out
方法 2:通過 -L 手動指定靜態庫連線地址:
gcc main.c -lmath -L. // -l 會自動加上 lib 和 .a;
./a.out
方法 3:設定臨時環境變數:
export LIBRAR_PATH=$LIBRART_PATH:. // gcc 連結時查詢靜態庫/共享庫的路徑
gcc main.c -lmath
./a.out
方法 4:也可以在 ~/.bashrc 或 ~/.bash_profile,配置檔案中寫環境變數,持久有效:
#ls -a
#vi .bashrc // 進入 .bashrc
或者:#vi .bash_profile
在 bashrc 裡輸入下面語句:
export CPATH=$CPATH:.
退出 bashrc,執行下面語句:
#source ~/.bashrc
或者:#source ~/.bash_profile
檢視結果:
#echo $CPATH
執行靜態庫
=====
# ./a.out
在可執行程式的連結階段,已將所呼叫的函式的二進位制程式碼,
複製到可執行程式中,因此執行時不需要依賴靜態庫。
14. 動態庫
1)建立動態庫:
=======
把上面所有的標頭檔案和目標檔案都拷貝一份,作如下操作:
gcc -c -fpic calc.c --> calc.o
gcc -c -fpic show.c --> show.o // 編譯器
gcc -shared calc.o show.o -o libmath.so --> libmath.so // 連結器
或一次完成編譯和連結:
# gcc -shared -fpic calc.c show.c -o libmath.so
PIC (Position Independent Code):位置無關程式碼。
可執行程式載入它們時,可將其對映到其地址空間的任何位置。
-fPIC : 大模式,生成程式碼比較大,執行速度比較慢,所有平臺都支援。
-fpic : 小模式,生成程式碼比較小,執行速度比較快,僅部分平臺支援。
注意:提供共享庫的同時也需要提供標頭檔案。
2)呼叫動態庫
=======
方法 1:
# gcc main.c libmath.so (直接法)
方法 2:
通過 LIBRARY_PATH 環境變數指定庫路徑:
# export LIBRARY_PATH=$LIBRARY_PATH:.
# gcc main.c -lmath (環境法)
方法 3:
通過 gcc 的 -L 選項指定庫路徑:
# unset LIBRARY_PATH // 取消方法 2 中設定的環境變數
# gcc main.c -lmath -L. (引數法)
方法 4:也可以在 ~/.bashrc 或 ~/.bash_profile,配置檔案中寫環境變數,
方法同上;
方法 5:
一般化的方法:
# gcc .c/.o -l<庫名> -L<庫路徑>
3)執行動態庫
=======
執行時需要保證 LD_LIBRARY_PATH 環境變數中包含共享庫所在的路徑:
# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
# ./a.out
在可執行程式的連結階段,並不將所呼叫函式的二進位制程式碼複製到可執行程式中,
而只是將該函式在共享庫中的地址嵌入到可執行程式中,因此執行時需要依賴共享庫。
15. 動態載入共享庫/動態庫
#include <dlfcn.h>
1)載入共享庫
void* dlopen (const char* filename, int flag);
成功返回共享庫控制代碼,失敗返回 NULL;
filename 為共享庫路徑,若只給檔名,則根據 LD_LIBRARY_PATH 搜尋;
flag取值:
RTLD_LAZY - 延遲載入,使用共享庫中的符號(如呼叫函式)時才載入。
RTLD_NOW - 立即載入。
2)獲取函式地址
void* dlsym (void* handle, const char* symbol);
成功返回函式地址,失敗返回 NULL;
handle 為共享庫控制代碼,symbol 為函式名;
3)解除安裝共享庫
int dlclose (void* handle);
成功返回 0,失敗返回非零。
4)獲取錯誤資訊
char* dlerror (void);
有錯誤發生則返回錯誤資訊字串指標,否則返回 NULL。
範例:load.c
#include <stdio.h>
#include <dlfcn.h>
typedef int (*PFUN_CALC) (int, int); // 定義函式指標型別
typedef void (*PFUNC_SHOW) (int, char, int, int);
int main () {
// 建立控制代碼
void* handle = dlopen ("shared/libmath.so", RTLD_NOW);
if (! handle) {
fprintf (stderr, "dlopen:%s\n", dlerror ());
return -1;
}
// 取出函式
PFUNC_CALC add = (PFUNC_CALC)dlsym (handle, "add");
if (! add) {
fprintf (stderr, "dlsym:%s\n", dlerror ());
return -1;
}
PFUNC_CALC sub = (PFUNC_CALC)dlsym (handle, "sub");
if (! sub) {
fprintf (stderr, "dlsym:%s\n", dlerror ());
return -1;
}
PFUNC_SHOW show = (PFUNC_CALC)dlsym (handle, "show");
if (! show) {
fprintf (stderr, "dlsym:%s\n", dlerror ());
return -1;
}
// 呼叫
show (30, '+', 20, add (30, 20));
show (30, '+', 20, sub (30, 20));
// 關閉
if (dlclose (handle)) {
fprintf (stderr, "dlclose:%s\n", dlerror ());
return -1;
}
return 0;
}
# gcc load.c -ldl
注意:連結時不再需要 -lmath,但需要 -ldl(連線動態庫)。
16. 輔助工具
nm: 檢視目標檔案、可執行檔案、靜態庫、共享庫中的符號列表。
ldd: 檢視可執行檔案和共享庫的動態依賴。
ldconfig: 共享庫管理。
事先將共享庫的路徑資訊寫入 /etc/ld.so.conf 配置檔案中,
ldconfig 根據該配置檔案生成 /etc/ld.so.cache 緩衝檔案,
並將該緩衝檔案載入記憶體,藉以提高共享庫的載入效率。
系統啟動時自動執行 ldconfig,但若修改了共享庫配置,則需要手動執行該程式。
strip: 減肥。去除目標檔案、可執行檔案、靜態庫和共享庫中的符號列表、除錯資訊等。
objdump: 顯示二進位制模組的反彙編資訊。
# objdump -S a.out
指令地址 機器指令 彙編指令
-------- -------------------- ---------------------
8048514: 55 push %ebp
8048515: 89 e5 mov %esp,%ebp
8048517: 83 e4 f0 and $0xfffffff0,%esp
804851a: 83 ec 20 sub $0x20,%esp
804851d: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
17. 作業:編寫一個函式 diamond(),列印一個菱形,其高度、
寬度、實心或者空心以及圖案字元,均可通過引數設定。
分別封裝為靜態庫 libdiamond_static.a 和動態庫 libdiamond_shared.so,並呼叫之。
標頭檔案:diamond.h
#ifndef _DIAMOND_H
#define _DIAMOND_H
void diamond (int h, int d, int s, char c);
#endif // _DIAMOND_H
檔案:diamond.c
#include <stdio.h>
#include "diamond.h"
void diamond (int h, int d, int s, char c) {
int i, j;
for (i = 0; i <= h; i++) {
printf ("%*s", (h - i) * d, "");
for (j = 0; j < i * d * 2; j++)
printf ("%c", j ? (s ? c : ' ') : c);
printf ("%c\n", c);
}
for (i = 1; i <= h; i++) {
printf ("%*s", i * d, "");
for (j = 0; j < (h - i) * d * 2; j++)
printf ("%c", j ? (s ? c : ' ') : c);
printf ("%c\n", c);
}
}
主函式:main.c
#include <stdio.h>
#include "diamond.h"
int main (int argc, char* argv[]) {
if (argc < 5) {
fprintf (stderr, "Usage: %s <H> <D> <S> <C>\n", argv[0]);
return -1;
}
diamond (atoi (argv[1]), atoi (argv[2]), atoi (argv[3]), *argv[4]);
return 0;
}