0
C 代码是如何跑起来的
source link: https://segmentfault.com/a/1190000039890899
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
上一篇「CPU 提供了什么」中,我们了解了物理的层面的 CPU,为我们提供了什么。
本篇,我们介绍下高级语言「C 语言」是如何在物理 CPU 上面跑起来的。
C 语言提供了什么
C 语言作为高级语言,为程序员提供了更友好的表达方式。在我看来,主要是提供了以下抽象能力:
- 变量,以及延伸出来的复杂结构体
我们可以基于变量来描述复杂的状态。 - 函数
我们可以基于函数,把复杂的行为逻辑,拆分到不同的函数里,以简化复杂的逻辑以。以及,我们可以复用相同目的的函数,现实世界里大量的基础库,简化了程序员的编码工作。
构建一个良好的示例代码,可以很好帮助我们去理解。
下面的示例里,我们可以看到 变量 和 函数 都用上了。
#include "stdio.h"
int add (int a, int b) {
return a + b;
}
int main () {
int a = 1;
int b = 2;
int c = add(a, b);
printf("a + b = %d\n", c);
return 0;
}
毫无意外,我们得到了期望的 3
。
$ gcc -O0 -g3 -Wall -o simple simple.c
$ ./simple
a + b = 3
我们还是用 objdump
来看看,编译器生成了什么代码:
- 变量
局部变量,包括函数参数,全部被压入了 栈 里。 - 函数
函数本身,被单独编译为了一段机器指令
函数调用,被编译为了call
指令,参数则是函数对应那一段机器指令的第一个指令地址。
$ objdump -M intel -j .text -d simple
# 截取其中最重要的部分
000000000040052d <add>:
40052d: 55 push rbp
40052e: 48 89 e5 mov rbp,rsp
400531: 89 7d fc mov DWORD PTR [rbp-0x4],edi
400534: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
400537: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
40053a: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]
40053d: 01 d0 add eax,edx
40053f: 5d pop rbp
400540: c3 ret
0000000000400541 <main>:
400541: 55 push rbp
400542: 48 89 e5 mov rbp,rsp
400545: 48 83 ec 10 sub rsp,0x10
400549: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1
400550: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
400557: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8]
40055a: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
40055d: 89 d6 mov esi,edx
40055f: 89 c7 mov edi,eax
400561: e8 c7 ff ff ff call 40052d <add>
400566: 89 45 f4 mov DWORD PTR [rbp-0xc],eax
400569: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]
40056c: 89 c6 mov esi,eax
40056e: bf 20 06 40 00 mov edi,0x400620
400573: b8 00 00 00 00 mov eax,0x0
400578: e8 93 fe ff ff call 400410 <printf@plt>
40057d: b8 00 00 00 00 mov eax,0x0
400582: c9 leave
400583: c3 ret
400584: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
40058b: 00 00 00
40058e: 66 90 xchg ax,ax
函数内的局部变量,为什么会放入栈空间呢?
这个刚好和局部变量的作用域关联起来了:
- 函数执行结束,返回的时候,局部变量也应该失效了
- 函数返回的时候,刚好要恢复栈高度到上一个调用者函数。
这样的话,只需要栈高度恢复,也就意味着被调用函数的所有的临时变量,全部失效了。
函数内的局部变量,一定会放入栈空间吗?
答案是,不一定。
上面我们是通过 -O0
编译的,接下来,我们看下 -O1
编译生成的机器码。
此时的局部变量直接放在寄存器里了,不需要写入到栈空间了。
不过,此时 main
都已经不再调用 add
函数了,因为已经被 gcc 内联优化了。
好吧,构建个合适的用例也不容易。
000000000040052d <add>:
40052d: 8d 04 37 lea eax,[rdi+rsi*1]
400530: c3 ret
0000000000400531 <main>:
400531: 48 83 ec 08 sub rsp,0x8
400535: be 03 00 00 00 mov esi,0x3
40053a: bf f0 05 40 00 mov edi,0x4005f0
40053f: b8 00 00 00 00 mov eax,0x0
400544: e8 c7 fe ff ff call 400410 <printf@plt>
400549: b8 00 00 00 00 mov eax,0x0
40054e: 48 83 c4 08 add rsp,0x8
400552: c3 ret
400553: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
40055a: 00 00 00
40055d: 0f 1f 00 nop DWORD PTR [rax]
禁止内联优化
我们用如下命令,关闭 gcc 的内联优化:
gcc -fno-inline -O1 -g3 -Wall -o simple simple.c
再来看下汇编代码,此时的机器码就符合理想的验证结果了。
000000000040052d <add>:
40052d: 8d 04 37 lea eax,[rdi+rsi*1]
400530: c3 ret
0000000000400531 <main>:
400531: 48 83 ec 08 sub rsp,0x8
400535: be 02 00 00 00 mov esi,0x2
40053a: bf 01 00 00 00 mov edi,0x1
40053f: e8 e9 ff ff ff call 40052d <add>
400544: 89 c6 mov esi,eax
400546: bf f0 05 40 00 mov edi,0x4005f0
40054b: b8 00 00 00 00 mov eax,0x0
400550: e8 bb fe ff ff call 400410 <printf@plt>
400555: b8 00 00 00 00 mov eax,0x0
40055a: 48 83 c4 08 add rsp,0x8
40055e: c3 ret
40055f: 90 nop
- 对于 C 语言的变量,编译器会为其分配一段内存空间来存储
函数内的局部变量,放入栈空间是理想的映射方式。不过编译的优化模式下,则会尽量使用寄存器来存储,寄存器不够用了,才会使用栈空间。
全局变量,则有对应的内存段来存储,这个以后可以再聊。 - 对于 C 语言的函数,编译器会编译为独立的一段机器指令
调用该函数,则是执行call
指令,意思是接下来跳转到执行这一段机器指令。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK