1 程序的加载、运行
编译得到可执行目标文件后,就可以将“可执行目标文件”加载“运行地址”所指的内存位置,然后运行了。
不过这里还是要分两种情况来看,第一种是裸机运行的情况,第二种是基于OS虚拟内存运行的情况。
(1)裸机的情况
使用专门针对裸机的编译器来编译程序,最后得到的就是可以在裸机上运行的可执行程序。
加载裸机程序时,由专门的加载程序(加载软件)来实现的。
如果你无法想象裸机程序是如何加载的话,你就想一想单片机程序的加载,因为单片机其实就是裸机的情况。
1)加载
其实加载的过程就是将“代码段”和“数据段”复制到内存上
裸机时,链接器重定位后的“运行地址”是真实的物理地址,加载时直接将“代码段”和“数据段”复制到物理内存中“运行地址”所指定的位置。
裸机运行地址是多少,可以由我们程序员自己来定。
注意:裸机时就不是ELF格式头了,而是bin格式头。
2)运行
(a)CPU的PC(程序计数器)存放第一条指令_start的地址,也就是将PC指向第一条指令_start。
pc是cpu的寄存器之一。
(b)从_start开始执行启动代码。
(c)启动代码调用_init等函数进行初始化。
初始化有一件非常重要的事情就是,从内存划出一片空间出来用作堆和栈,因为空间是以堆和栈的方式来管理的,因此就称为堆 和 栈。
(d)启动代码调用main函数,main函数再调用各个子函数,我们自己写的代码就开始运行了。
(e)main函数调用return关键字,返回到启动代码。
对于裸机的来说,返回到启动代码就结束了。
至于return的返回值,有没有返回值,对于裸机来说都没有什么影响。
就算有返回值,将返回值返回给启动代码后,这个返回值对启动代码来说也没有什么意义。
所以说,对于裸机来说,其实main函数的返回值没有什么意义,所以大家在学习单片机时候,以前的main函数的返回值都是void的。
void main(void) { return; }
不过现在都规范化了,单片机等裸机里面,也要求main函数的返回值类型为int型。
int main(void) { return 0; }
尽管在这里要求返回int型的返回值,但是我们自己应该清楚,在裸机下,main函数的返回值并没有什么大的意义。
3)栈、堆
前面说过,程序运行起来后,初始化代码会从内存中划出一片空间,用来作为程序运行所需要的栈和堆。
(a)栈(stack)
栈的意思是,表示内存空间以栈这种数据结构来进行管理,所谓管理就是管理空间的开辟和释放。
学过栈这种数据结构的同学都知道,栈的特点是,只能在栈顶进行操作,不能够在栈的中间和栈底操作。
· 栈是向下生长的
所谓向下生长就是,栈底在最高地址处,当栈中没有任何空间被使用时,栈顶指针就指向栈底,每当栈顶被占用一个字节的空间,栈顶指针就向低地址方向移动一个字节。
从高地址向低地址方向移动,就是向下生长,栈顶指针所指的那个字节是没被用的。
栈顶和栈底之间的栈空间,就是被占用的空间。
反过来,栈顶指针向高地址后退一个字节,就表示释放一个字节的空间。
释放的意思就是将空间交出去,让别人可以使用。
怎么理解栈顶指针?
就是某个寄存器或则指针变量,专门用于存放栈顶字节的地址。
· 栈的作用
函数自动局部变量、形参等就开辟于栈。
int fun(int a) { int b; ... }
不过这里有一点需要强调下,对于ARM来说,由于arm cpu内部寄存器比较多,所以如果形参在4个以内的,实际上形参是在寄存器中,而并不在栈中。
如果超过4个的话,第4个往后的形参才会存在栈中。
不过在intel的CPU上又不一样,因为Intel cpu的寄存器比较紧俏,所以形参基本都是存在栈中的。
我们这里为了讲课的方便,我们一律认为形参都是在栈中的。
从栈中开辟和释放自动局部变量、形参空间的过程,由函数被调用时,在运行的过程中自动完成的,
无需程序员关心,开辟空间和释放空间的本质,其实就是栈顶指针移动的过程。
(b)堆(heap)
堆空间和栈空间的管理方式是有区别的。
· 栈的话只能在栈顶才能进行操作,但是堆不是,堆的话可以在中间任何位置操作。
· 堆的空间是向上生长的,也就是说在堆中开辟空间时与栈相反,是从低地址往高地址方向延伸的。
· 栈的空间是自动开辟和释放的,但是堆的空间不是的,堆只能手动开辟和释放。
- 从堆里面开辟空间
程序需要调用malloc函数来手动开辟。
所谓手动开辟,就是程序员需要在程序中亲自调用某个函数来实现,至于说在堆中什么位置开辟空间,这个由malloc函数的算法来决定。
- 释放在堆中开辟的空间:在程序中调用free函数,手动释放
释放的意思,也是将空间让出来,让别人可以使用。