C语言算是大学里接触的最早,用的最"多"的语言了,对于大部分学习计算机的学生基本上是从开始学习C语言起,凭借着一句经典的"hello, world!"迈入了计算机的世界的,初体味了一把这个世界还有个叫编程的活。作为系统级的开发首选语言,只诞生以来就屹立不倒,C语言的重要性是不言而喻的。就是怀着这种对C的无比敬意开始了我的伪程序之旅。然而大学里面没写过什么像样的东西,说来惭愧,什么课程设计,或是自称为项目的东西大都由些蹩脚的程序拼凑而成。做为一个菜鸟级别的程序员,使用C有些年,但对于C没有有真正的了解。我想有必要从新了解这门古老的语言背后的东西,知其然还要知其所以然,才能更好的使用这门语言。当然语言是工具,但了解工具的强项、陷阱与缺陷,对于工具威力的发挥 ,对于你去驾驭工具的娴熟程度是那是大有裨益啊。C语言的设计哲学就是给你一把锤子嘛, 用不好可是会砸自己的脚。
C程序编译流程
编译一个C程序可以分为四阶段,预处理阶段->生成汇编代码阶段->汇编阶段->链接阶段,这里以linux环境下gcc编译器为例。使用gcc时默认会直接完成这四个步骤生成可以执行的程序,但通过编译选项可以控制值进行某些阶段,查看中间的文件。

gcc指令的一般格式为:
gcc [选项] 要编译的文件 [选项] [目标文件]
其中,目标文件可缺省,gcc默认生成可执行的文件名为:a.out
gcc main.c 直接生成可执行文件a.out
gcc -E main.c -o hello.i 生成预处理后的代码(还是文本文件)
gcc –S main.c -o hello.s 生成汇编代码
gcc –c main.c -o hello.o 生成目标代码
C程序目标文件和可执行文件结构
目标文件和可执行文件可以有几种不同的格式,有ELF(Excutable and linking Format,可执行文件和链接)格式,也有COFF(Common Object-File Format,普通目标文件格式)。虽然格式不一样,但具有一个共同的概念,那就是段(segments),这里段值二进制格式文件中的一块区域。
linux下的可执行文件有三个段文本段(text)、数据段(data)、bss段,可用nm命令查看目标文件的符号清单。
编译过程: 源文件-------->到可执行文件

其中注意的BSS段,并没有保存未初始化段的映像,只是记录了该段的大小(应为该段没有初值,不管具体值),到了运行时再到内存为未初始化变量分配空间,这样可以节省目标文件空间。对于data段,只是保存在目标文件中,运行时直接载入。
C程序的内存布局
运行过程: 可执行文件->内存空间

对于data段,保存的是初始化的全局变量和stataic的局部变量,直接载入内存即可。 text段保存的是代码直接载入。BSS段从目标文件中读取BSS段大小,然后在内存中紧跟data段之后分配空间,并且清零(这也是为什么全局表量和static局部变量不初始化会有0值得原因)
函数调用栈
作为面向过程的语言,C基本的特色就是模块化、过程化。一个C程序或一个模块由一堆函数组成,然后程序执行,按代码的结构调用这些函数,完成功能。那么函数调用的背后编译器到底为我们做了什么呢?
void fun(int a, double b)
{
int c = 300;
c += 1;
}
int main()
{
fun(100, 200);
return 0;
}
{
int c = 300;
c += 1;
}
int main()
{
fun(100, 200);
return 0;
}
.globl _fun ;全局函数符号
.def _fun;
_fun: ;函数fun入口
pushl %ebp ;保存ebp值
movl %esp, %ebp ;采用ebp来访问栈顶
subl $4, %esp ;esp用来扩展堆栈分配局部变量空间
movl $300, -4(%ebp) ;局部变量赋值
leal -4(%ebp), %eax ;得到局部变量有效地址
incl (%eax) ;访问局部变量
leave ;相当于movl ebp, esp pop ebp
ret
.def _fun;
_fun: ;函数fun入口
pushl %ebp ;保存ebp值
movl %esp, %ebp ;采用ebp来访问栈顶
subl $4, %esp ;esp用来扩展堆栈分配局部变量空间
movl $300, -4(%ebp) ;局部变量赋值
leal -4(%ebp), %eax ;得到局部变量有效地址
incl (%eax) ;访问局部变量
leave ;相当于movl ebp, esp pop ebp
ret
.globl _main
.def _main;
_main: ;main函数入口
;....
movl $200, 4(%esp) ; 参数入栈
movl $100, (%esp) ; 参数入栈
call _fun
.def _main;
_main: ;main函数入口
;....
movl $200, 4(%esp) ; 参数入栈
movl $100, (%esp) ; 参数入栈
call _fun
;.....

函数调用过程:
参数按从右到左顺序放到栈顶上
call调用,将返回地址ip入栈保存
在栈上分配局部变量空间
执行函数操作
函数返回过程:
ret会从栈上弹出返回地址
执行调用前后面的代码
由此得的结论是,函数调用一个动态的过程,调用的时候又有一个栈帧,调用的时候展开,结束的时候收缩。局部变量在运行到该函数的时候在栈上分配内存,这些内存实际上没有名字的,不同于数据段,有符号名字,局部变量在函数结束就销毁了。这也是什么局部变量同名互补干涉的原因,因为编译以后 ,根本就不是通过名字来访问的。
全局变量
全局变量有初始化或未初始化之分,初始化了的全局变量保存在data段,未初始化全局变量保存在BSS段,data段和BSS段都是程序的数据段
int global1 = 100;
int main()
{
global1 = 101;
extern int global2;
global2 = 201;
return 0;
}
int global2 = 200;
int main()
{
global1 = 101;
extern int global2;
global2 = 201;
return 0;
}
int global2 = 200;
.globl _global1 ;全局符号global1
.data ;位于数据段
.align 4
_global1:
.long 100 ;全局变量初值
;.....
.globl _main ;全局符号main
.def _main; ;是一个函数
_main: ;函数入口
;...
movl $101, _global1 ;通过符号访问全局变量
movl $201, _global2 ;通过符号访问全局变量,这个变量还未定义
movl $0, %eax
leave
ret
.globl _global2 :全局符号golbal2
.data ;位于数据段
.align 4
_global2: ;全局变量的定义,初始化值
.long 200
int global1;
int main()
{
global1 = 101;
extern int global2;
global2 = 201;
return 0;
}
int global2;
int main()
{
global1 = 101;
extern int global2;
global2 = 201;
return 0;
}
int global2;
.globl _main
.def _main;
_main:
;....
movl $101, _global1 ;通过符号访问全局变量,这个符号可以在之后,或其他文件中定义
movl $201, _global2
movl $0, %eax
leave
ret
.comm _global1, 16 # 4 ;标明这是个未初始化全局变量,声明多个,但最后运行时在bss段分配空间
.comm _global2, 16 # 4
可以得出结论:全局变量独立于函数存在,所有函数都可以通过符号访问,并且在运行期,其地址不变。
编译与链接
看下面这个程序链接出错,找不符号a,print, 但生成汇编代码并没有问题。这是因为编译的时候只是把符号地址记录下来,等到链接的时候该符号定义了才会变成具体的地址。如果链接的时候所有符号地址都有定义,那么生成可执行文件。如果有不确定地址的符号,则链接出错。
#include<stdio.h>
int main()
{
extern int a ;
print("a = %d\n", a);
return 0;
}
int main()
{
extern int a ;
print("a = %d\n", a);
return 0;
}
.file "fun.c"
.def ___main;
.section .rdata,"dr"
LC0:
.ascii "a = %d\12\0"
.text
.globl _main
.def _main; .
_main:
;..
movl _a, %eax ;通过符号访问全局变量a
movl %eax, 4(%esp)
movl $LC0, (%esp)
call _print ;通过符号访问函数print
movl $0, %eax
leave
ret
.def _print; ;说明print是个函数符号
.def ___main;
.section .rdata,"dr"
LC0:
.ascii "a = %d\12\0"
.text
.globl _main
.def _main; .
_main:
;..
movl _a, %eax ;通过符号访问全局变量a
movl %eax, 4(%esp)
movl $LC0, (%esp)
call _print ;通过符号访问函数print
movl $0, %eax
leave
ret
.def _print; ;说明print是个函数符号
全局变量的链接属性
全局变量的默认是extern的,最终存放在数据段,整个程序的所有文件都能访问,如果加上static则表明值能被当前文件访问。
#include<stdio.h>
static int a = 10;
int main()
{
a = 20;
return 0;
}
static int a = 10;
int main()
{
a = 20;
return 0;
}
.data
.align 4
_a: ;全局变量a定义,少了glbal的声明
.long 10
.def ___main;
.text
.globl _main
.def _main;
_main:
; ...
movl $20, _a
movl $0, %eax
去掉int a前面的static产生的汇编代码为:
.globl _a ; global声明符号 a为全局
.data
.align 4
_a:
.long 10
.def ___main
.text
.globl _main
.def _main
_main:
;...
call __alloca
call ___main
movl $20, _a
movl $0, %eax
.data
.align 4
_a:
.long 10
.def ___main
.text
.globl _main
.def _main
_main:
;...
call __alloca
call ___main
movl $20, _a
movl $0, %eax
对于未初始化全局变量
#include<stdio.h>
static int a;
int main()
{
a = 20;
return 0;
}
static int a;
int main()
{
a = 20;
return 0;
}
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
;..
movl $20, _a
movl $0, %eax
leave
ret
.lcomm _a,16 ; 多了个l表明是local的未初始化全局变量
.def _main; .scl 2; .type 32; .endef
_main:
;..
movl $20, _a
movl $0, %eax
leave
ret
.lcomm _a,16 ; 多了个l表明是local的未初始化全局变量
去掉int a前面的static
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
.def _main; .scl 2; .type 32; .endef
_main:
;..
movl $20, _a
movl $0, %eax
leave
ret
.comm _a, 16 # 4 ;extern链接属性的未初始化全局变量
movl $20, _a
movl $0, %eax
leave
ret
.comm _a, 16 # 4 ;extern链接属性的未初始化全局变量
static局部变量
static局部变量具备外部变量的生存期,但作用域却和局部变量一样,离开函数就能访问
#include<stdio.h>
int fun()
{
static int a = 10;
return (++a);
}
int main()
{
printf("a = %d\n",fun());
printf("a = %d\n",fun());
}
int fun()
{
static int a = 10;
return (++a);
}
int main()
{
printf("a = %d\n",fun());
printf("a = %d\n",fun());
}
.data
.align 4
a.0: ;static局部变量是放在代码段
.long 10 ;分配空间初始化
.text
.globl _fun
.def _fun;
_fun:
pushl %ebp
movl %esp, %ebp
incl a.0
movl a.0, %eax
popl %ebp
ret
.def ___main;
.section .rdata,"dr"
.align 4
a.0: ;static局部变量是放在代码段
.long 10 ;分配空间初始化
.text
.globl _fun
.def _fun;
_fun:
pushl %ebp
movl %esp, %ebp
incl a.0
movl a.0, %eax
popl %ebp
ret
.def ___main;
.section .rdata,"dr"
编译实际还是还是把static局部变量放在数据段存储(要么怎么可能在程序运行期间地址不变呢),值不过符号名会动点手脚(这样出了函数就访问不了了),同时候 多个函数中定义同名的static局部变量,实际上是不同的内存单元,互补干涉了。