深度

main函数

void main()

“The definition void main( ) { /* … */ } is not and never has been C++, nor has it even been C.” C 和 C++ 标准中没有 void main()的规定。
在有些编译器中 void main()可以通过,并不代表所有。

main()

规定:main如果不明确标明返回值,默认返回值为int,也就是说 main()等同于int main(),而不是等同于void main()。

C和C++的标准

main 函数的返回值类型必须是 int ,这样返回值才能传递给程序的调用者(如操作系统),等同于 exit(0),来判断函数的执行结果。

return 语句

如果 main 函数的最后没有写 return 语句的话,C99 和c++89都规定编译器要自动在生成的目标文件中加入return 0,表示程序正常退出。
return的返回值会进行 类型转换,比如:若return 1.2 ;会将其强制转换为1,即真正的返回值是1,同理,return ‘a’ ;的话,真正的返回值就是97;但是若return “abc”;便会报警告,因为无法进行隐式类型转换。

main函数传参

首先说明的是,可能有些人认为main函数是不可传入参数的,但是实际上这是错误的。main函数可以从命令行获取参数,从而提高代码的复用性。
可选的main函数原形为:

1
int main(int argc , char* argv[],char* envp[]);

①、第一个参数argc表示的是传入参数的个数 。
②、第二个参数char* argv[],是字符串数组,用来存放指向的字符串参数的指针数组,每一个元素指向一个参数。各成员含义如下:
argv[0]:指向程序运行的全路径名。
argv[1]:指向执行程序名后的第一个字符串 ,表示真正传入的第一个参数。
argv[2]:指向执行程序名后的第二个字符串 ,表示传入的第二个参数。
…… argv[n]:指向执行程序名后的第n个字符串 ,表示传入的第n个参数。
规定:argv[argc]为NULL ,表示参数的结尾。
③、第三个参数char* envp[],也是一个字符串数组,主要是保存这用户环境中的变量字符串,以NULL结束。envp[]的每一个元素都包含ENVVAR=value形式的字符串,其中ENVVAR为环境变量,value为其对应的值。
envp一旦传入,它就只是单纯的字符串数组而已,不会随着程序动态设置发生改变。可以使用putenv函数实时修改环境变量,也能使用getenv实时查看环境变量,但是envp本身不会发生改变;平时使用到的比较少。
注意:main函数的参数char* argv[]和char* envp[]表示的是字符串数组,书写形式不止char* argv[]这一种,相应的argv[][]和 char** argv均可。

main的执行顺序

为什么说main()是程序的入口

linux系统下程序的入口是”_start”,这个函数是linux系统库(Glibc)的一部分,当我们的程序和Glibc链接在一起形成最终的可执行文件的之后,这个函数就是程序执行初始化的入口函数。

  • 编译器缺省是找 __start 符号,而不是 main
  • __start 这个符号是程序的起始
  • main 是被标准库调用的一个符号
_start和main函数有什么关系

_start函数的实现该入口是由ld链接器默认的链接脚本指定的,当然用户也可以通过参数进行设定。
_start由汇编代码实现。大致用如下伪代码表示:

1
2
3
4
5
6
7
8
9
void _start()
{
%ebp = 0;
int argc = pop from stack
char ** argv = top of stack;
__libc_start_main(main, argc, argv, __libc_csu_init, __linc_csu_fini,
edx, top of stack);
}

对应的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_start:
xor ebp, ebp //清空ebp
pop esi //保存argc,esi = argc
mov esp, ecx //保存argv, ecx = argv

push esp //参数7保存当前栈顶
push edx //参数6
push __libc_csu_fini//参数5
push __libc_csu_init//参数4
push ecx //参数3
push esi //参数2
push main//参数1
call _libc_start_main

hlt

main函数运行之前的工作

main函数执行之前还要做一系列的工作。主要就是初始化系统相关资源:
1.设置栈指针
2.初始化static静态和global全局变量,即data段的内容
3.将未初始化部分的赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等,即.bss段的内容
4.运行全局构造器,类似c++中全局构造函数
5.将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数

mian函数执行之前运行的代码

(1)全局对象的构造函数会在main 函数之前执行。
(2)一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行main函数之前,而main函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作
(3)进程启动后,要执行一些初始化代码(如设置环境变量等),然后跳转到main执行。全局对象的构造也在main之前。
(4)通过关键字attribute,让一个函数在主函数之前运行,进行一些数据初始化、模块加载验证等。

main函数之后执行的函数

在main函数运行之后还有其他函数可以执行,main函数执行完毕之后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。
1、全局对象的析构函数会在main函数之后执行; 
2、用atexit注册的函数也会在main之后执行

函数

简介

  • c程序的基本构件–函数
  • 一个函数由函数首部和函数体构成:
    • 函数首部一般包括函数类型、函数名、函数参数等。
    • 函数体一般包括声明和执行部分。其中:在声明部分定义所用到的变量;执行部分则由若干个语句组成。

C语言中,函数调用包括三个步骤:

1.调用,调用者将参数传递给被调用者,并交出控制权。

2.执行,被调用者执行任务。

3.被调用者返回结果,并将控制权交还给调用者。

调用机制实现中,函数必须是“调用者无关的”,因为一个函数可能被不同的调用者调用。

运行时栈

首先,函数执行之前,必须在内存中为该函数的局部变量分配空间(其实就是存放函数的活动记录,局部变量都是位于活动记录中)
那么活动记录的内存空间怎么分配?
1.编译器负责分配,即在程序运行时,一个函数的活动记录的内存位置是固定不变的,这样会有什么问题呢?递归无法使用了,因为函数调用自身会导致调用者的活动记录的空间数据被新的调用给覆盖了,导致数据错乱。
2.函数在被调用时,每调用一次就分配一次内存空间。函数返回时,则将该活动记录空间返还,以供其他函数调用使用。这样的好处就是可以实现递归,当然实现相对会变得复杂。像C一样支持递归的语言,基本都是按照这个方案实现的。
为了降低方案2实现的复杂性,我们需要借助于“栈”,根据栈的特性,我们可以很容易的维护整个调用过程中的状态。

实现机制

函数调用时,底层需要做很多事情:形式参数传递,活动记录的入栈和出栈,控制权在函数之间的转交。
具体步骤:
1.调用者将参数拷贝到被调用者所能访问的内存区域。将参数从右往左依次压入运行时栈。
2.被调用者函数的开始,将活动记录压入栈,并在栈中保存一些备忘信息,保证执行完毕,控制权交还后调用者的状态(如局部变量,寄存器等等)没有变化。

1
2
3
4
- 首先为返回值预留一个内存位置(通过栈指针递减,将一个内存空间压入栈,这个位置由被调用函数在返回调用者之前,填入返回值)
- 随后保存调用者的地址,即将返回地址压入栈;
- 接着将调用者的帧指针压入栈, 备份调用者的帧指针非常重要,有时候称帧指针为“动态链”,它是保证调用者在重获控制权后,能恢复局部变量访问的关键。
- 然后在栈空间中,为被调用者自己的局部变量分配空间

3.被调用者执行代码完成自己的工作。
4.被调用函数完成后,将活动记录出栈,并将控制权返回调用者

1
2
3
4
5
- 如果存在返回值,填写活动记录的返回值字段。
- 将局部变量出栈。
- 恢复原动态链(调用者帧指针)内容。
- 恢复返回地址。
- 通过RET指令,返回调用者。

5.调用者获取控制权,读取被调用者的返回值,接着将返回值出栈,参数出栈。至此,整个函数调用过程结束。
!在函数执行过程中,R0-R3作用是存放计算中的临时值,R4-R7默认为系统功能,R4指向全局数据区段,R5为帧指针,R6为栈指针,R7为返回地址。R4-R7有明确的使用规定,不用额外考虑,而R0-R3怎么保证函数调用不会改变内容呢?两种办法,一种是存入调用者活动区,即“调用者保存”规则,函数返回时,调用者再将其弹出栈,恢复原本值;另一种就是存入被调用者活动区,“被调用者保存”规则,被调用函数初始化时存入,返回前恢复寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int func1(int x)
{
int c;
c = func2(x, 10);
return c;
}

int func2(int x, int y)
{
int m;
int k;
m = y + 10;
++x;

k = m + x;

return k;
}

int main()
{
int a;
int b;

a = 5;
b = 3;

a = func1(a);
func2(a, b);

return 0;
}

对于main函数来说,活动记录为压入a,压入b,执行到函数调用func1(a)时,将参数a压入运行时栈,由于R6总是指向栈顶,所以每压入一个数据项,首先递减R6(注意运行时栈内存地址从大到小),然后将入栈内容存入R6指向的地址。参数入栈后,就通过JSR指令,将控制权交给func1了;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
main:

....
ADD R6, R6, #-2 ;//分配a b的空间
LDR R0, R5, #0 ;
AND R0, R0, #0 ;
ADD R0, R0, #5 ; //a赋值
LDR R0, R5, #1 ;
AND R0, R0, #0 ;
ADD R0, R0, #3 ; //b赋值

LDR R0, R5, #0 ;
ADD R6, R6, #-1 ;
STR R0, R6, #0 ; //push a

JSR func1

LDR R0, R6, #0 ;读取返回值
STR R0, R5, #0 ;将返回值写给a
ADD R6, R6, #2 ;返回值和参数出栈


LDR R0, R5, #0 ;
ADD R6, R6, #-1 ;
STR R0, R6, #0 ; //push a
LDR R0, R5, #-1 ;
ADD R6, R6, #-1 ;
STR R0, R6, #0 ; //push b

JSR func2

;虽然func2有返回值,但是这里不需要,所以不用读取
ADD R6, R6, #3 ;//返回值和参数出栈

func1完成调用信息的备份操作,首先递减R6,压入一个内存地址用于存放返回值,再递减R6,将R7的内容push入栈,在递减R6,将R5(动态链)内容入栈。接着分配func1自身的局部变量只有一个c,所以只需要的空间1。接着是调用func2并将返回值赋给c(这一步的调用没什么区别,忽略说明),func1执行完毕后,将c存入返回值,然后将局部变量出栈(无论多少局部变量直接R6=R5+1即可,想想为什么),将动态链出栈,返回地址出栈,最后调用RET将控制权返还给main,调用结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func1:

ADD R6, R6, #-1 ;用于存放返回值的内存地址

ADD R6, R6, #-1 ;
STR R7, R6, #0 ; 将返回地址入栈备份

ADD R6, R6, #-1 ;
STR R5, R6, #0 ; 动态链内容入栈备份

ADD R5, R6, #-1 ; 将帧指针内容换成自己的
ADD R6, R6, #-1 ; 开辟空间用于存放自身局部变量c


... ;func2的调用这里省略

;调用结束时的处理
LDR R0, R5, #0 ;
STR R0, R5, #3 ;将c写入返回值

ADD R6, R5, #1 ;将局部变量出栈

LDR R5, R6, #0 ;
ADD R6, R6, #1 ;动态链出栈

LDR R7, R6, #0 ;
ADD R6, R6, #1 ;返回地址出栈

RET ; 返回main