"hello world"應該是每一個學過語言最最熟悉的面孔了。但是我們真的知道它是如何
print在我們面前的嗎?
首先我想說一下,一門進階語言,一段程式,真的很簡單,因為它僅僅是我們
人類的助記符罷了。原來在學習語言時,老有一個疑問。為什麼我寫一的一段CODE,計算
計為什麼可以知道我想讓它幫我完成是麼事情呢。感覺很神奇。現在總結一下,一段代碼
是如何變成了電腦可以認識,並可以執行的。
對於一個電腦(不單單是PC)不一定有可以輸出字串的裝置,那"hello world"
現在對我們來說也有點稍微難辦了。但是只要是一個電腦,不管它是再小,功能再簡單
的單片機,那它也應該有一個LED燈。那就以點亮LED燈這個比"hello world"還簡單的程式
作為例子說一下吧。
下面是我在AT89S52 CPU下,寫的一段LED閃爍的小例子,
/****************************************************************************
* led_flicker example 1
* description : led flick simple
* author : liyangth@gmail.com
* date: 12-3-2006
****************************************************************************/
#include "reg51.h"
typedef unsigned int uint_t;
sbit P1_0 = P1^0; /*P1寄存器接了8個LED小燈,我現在用它的第一個燈,即P^0位*/
void mydelay(uint_t);
void main(void)
{
for(;;){
P1_0 = !P1_0; /* 低電平0,led 亮;現在取反達到閃爍的效果 */
mydelay(1000); /* 當然沒有延時,我們的眼睛不可能比CPU的處理速度快,根本無法看到閃爍 */
}
}
/******************************************************************************
*
* Function: void mydelay(uint_t delay_time)
*
* Description: delay delay_time ms
*
* Param:
*
* Returns: void
*
* Note: delay;
*
* add: liyangth@mail.com 12-3
*
* modify:
*
*******************************************************************************/
void mydelay(uint_t delay_time)
{
uint_t i;
while(delay_time--){
for(i = 0; i < 500; i++) /* 你可以用秒錶自己測,我測了大概可以用掉1ms的時間了 */
;
}
}
現在一個LED的閃爍程式好了,那我們怎麼讓電腦上的LED按照我們的想法就真的去閃。這裡要經過三個過程。
1.編譯;
2.連結;
3.定址;
1.編譯:
編譯器來幫我們完成,它把我們上面可以讀懂的代碼,翻譯成特定處理器的等效的一系列作業碼;從某種意義上可以把它理解成彙編器。但是它只執行一個簡單的逐行把人可閱讀的助記符翻譯到對應的作業碼的過程;每一種處理器都有它自己的機器語言,所以在嵌入式領域,我經常要用到的是交叉編譯器。經過編譯後的代碼變成了一個目標檔案。objdump一下,可以看到你的代碼已經面目全非,其中所有代碼塊都被收集到一個text段中,所有已初始化的全域變數都被收集到data段中,那未初始化的呢,呵呵,他們在bss段。
通常目標檔案裡還有一個符號表,記錄了源檔案引用的所有變數和函數的名字和位置。但是這個表可能不完整,因為不是所有變數和函數都總在一個檔案裡定義。有些在別的檔案中的引用怎麼辦,那就靠連結器了,在連結器工作完後,不完整到完整。
2.連結:
連結器的輸出是一個同樣格式的目標檔案,但是它是將第一步編譯時間產生的目標檔案連結至完整後的圓滿體。連結器就是解決符號未引用問題。
在嵌入式編程中,你還要手動的把啟動代碼的目標檔案也用連結器將它一併算上。而且連結時的次序也是要它老大。(啟動代碼就是這麼吊)
那說一下啟動代碼究竟是啥。
為什麼我們寫的程式都要從一個叫main()的函數執行,這個函數有什麼吊的呢。 它吊的原因就是,啟動代碼最後會預設調用它。
說一下啟動代碼裡都幹了哪些事,
1)禁止所有中斷;
2)從rom複製所有初始化資料到ram裡;
3)把未初始化資料區清零;
4)為stack分配空間,並初始化;
5)初始化處理器的堆棧指標;
6)建立並初始化heap;
7)(只對C++)對所有全域變數執行建構函式和初始化函數;
8)允許中斷;
9)調用main函數;
看看,啟動代碼還真幹了不少我們不知道的事情呢。做好人,不留名;
3)定址:
經過了連結後,連結器為我們產生了一個程式的特殊的“可重定位”的拷貝。 換句話說,程式離完整
只差一步--給其內部的代碼和資料指定儲存區地址! (如果你不是一個嵌入式工作者,只是一個在PC上編程
的人員來說,現在你已經完整了。)
把可重定址程式轉換到一個可執行檔二進位映像的工具叫,定址器。它負責這三步中最容易的一步,就是給
代碼指定實際的物理地址。但實際上,這一步又是我們工作量最大的一步。我們要為定址器提供目標板上的儲存
器的資訊。定址器按照你提供的資訊將可重定址程式中的每一個代碼和資料指定到物理地址上去。然後產生一個包含二進位記憶體映像的輸出檔案,這個檔案就可以down到目標板的rom中,然後跑起來了。
怎麼告訴定址器儲存空間的資訊呢,有一個傳說中的指令碼。我們管他叫串連指令碼。在linux下叫ld串連指令碼。
這個指令碼怎麼編,可以google.在這提示一下,就是以section的形式。 其中有一點,有些以底線開始的名字,
是可以從你的原始碼內部引用的變數。連結器將用這些符號來解決輸入的目標檔案裡的引用。 這樣,比方說,
可能有嵌入式軟體的某一部分(通常在啟動代碼裡)把ROM裡可初始化的變數的初始值拷貝到RAM裡的資料區段中。
這個操作的開始和停止地址可以通過引用整型變數(_DataStart 和_DataEnd)符號化的建立。
現在終於完整了,一段代碼(一段我們可以看懂的助記符)就這樣在板子上裸奔了!