"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)符号化的建立。
现在终于完整了,一段代码(一段我们可以看懂的助记符)就这样在板子上裸奔了! *博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。