少点代码,多点头发linux
本文已经收录至个人GitHub,欢迎你们踊跃star 和 issues。c++
面试官超级喜欢问hello world问题 特别是校招,我校招碰到过3次github
其实不少看起来顺其天然简单的东西,背后是一套复杂的学问面试
记得很清楚第一次面试阿里巴巴的时候,面试官上来让我写一个hello world程序shell
当时我真的一面黑人问号的确认了三遍,面试官依旧淡定的说 是的 编程
写完就让我聊hello world,一个hello world聊了一个小时数据结构
那时候面试是校招实习,聊完我真的怀疑人生了 架构
这个问题很是考验应试者的计算机基础、自学能力以及对问题钻研的能力app
要回答好这个问题,必须掌握计算机基础、操做系统、编译原理等知识才能给出一个完美的答案
来了,开聊了,还没关注个人记得关注我,一键三连
代码如上,如今看来很简单 怎么也不会想到这样的程序还会出错
不丢人的说,龙叔第一次在写这段代码的时候,这个简单的程序大概写了三四遍
好不容易倒腾完了,点击运行后 发现少了头文件
加上以后再运行,发现少告终尾的 ; 号
加上以后,发现少了return 0
就这样倒腾了好几遍,终于在控制台输出了hello world!!! ,那一刻我激动得笑出了声
因而骄傲的我赶忙趁热打铁,写了下面的版本
这两个版本的代码都是C语言写的,C语言课程应该是大学的通识课了,用这个语言讲,你们都能看的明白
运行结果:
外甥很是好奇,这hello world究竟是怎么输出到屏幕的
龙叔也好奇过这个问题,只不过是在C语言学完以后才开始好奇
从冯·诺依曼的结构咱们能够知道,计算机的基本组成部分以下:
程序,首先是经过输入设备,鼠标、键盘输入的
写好的代码在文本文件中,是须要存储的,此时就用到存储器,代码是存储在磁盘中的
当你点击运行时,你的代码会被读到内存中,在内存中的代码会通过编译器进行编译为可执行文件
编译后的文件经操做系统的进程去启动一个用户进程执行用户的可执行程序
中央处理器会去处理程序逻辑,将执行结果输出到输出设备即显示器
每一个部分都有本身的工做,恪尽职守,这个在系统设计上叫模块清晰、功能完整
接下来就从几个方面好好说说这个 hello world,让面试官目瞪口呆下
代码输入这么简单的问题,还用龙叔讲??
如上图首先说下输入过程,此图作了一个浓缩,主要部件 键盘、主机(CPU、内存、磁盘)、显示器
代码输入过程看起来是蛮简单的,打开一个编辑器或者IDE,便可开始代码输入
刚开始学习推荐使用IDE,固然不是没有IDE就不能写代码
任何一个文本编辑器均可以进行代码输入
IDE(Integrated Development Environment) 集成开发环境,通常包括代码编辑器、编译器、调试器和图形用户界面等工具
好比写C&C with class 会下载 vc++、devC++、VS、Clion等等软件,很棒,工具能提升生产力
我习惯用Clion,IDE都是根据本身的须要来选择,用着爽就行
IDE是一个软件,集成度很高的软件 ,启动IDE意味着操做系统必须启动一个进程 该进程叫IDE进程
既然是集成 内部还有不少线程负责集成模块的工做
关于进程、线程 深层次的内容,后面文章会详细讲出 这里就先不展开了
IDE进程会被操做系统管理和调度
要明白这个问题得先说说键盘工做原理
键盘的基本原理就是实时监控按键,将按键信息送入计算机
在键盘的内部设计中有定位按键位置的键位扫描电路,当任何键被按下是 编码电路就会产生代码,这些代码会被送入接口电路,这些电路被称为键盘控制电路
根据键盘工做原理,分为编码键盘和非编码键盘
编码键盘:键盘控制电路的功能彻底依靠硬件来自动完成 ,根据按键自动识别编码信息
非编码键盘:键盘控制电路的功能依靠 硬件 和 软件 共同完成
监控键盘的原理就是电位扫描,电位扫描分为逐行扫描法和行列扫描法
原来如此,原来键盘是这样工做的,今后我在飞速敲击键盘时 会更有力量了
这仅仅是键盘驱动进程拿到键盘输入的结果,应用程序是如何得到输入数据的呢?
键盘后台进程拿到结果后会放在本身的共享内存中,应用程序经过共享内存获取到键盘输入结果
上图中很明显看到键盘输入是会发生IO操做的,IO总体内容这里不展开,后面文章会更新
一顿操做,此时IDE会拿到键盘输入的代码,你的hello world代码终于在显示器中让你看到了
接下来讲说躺在IDE中代码是如何运行出结果的
代码终因而敲好了,激动的你通常会想着要运行一手,火烧眉毛看到结果
别急再等等,咱们书写的代码程序被称为源代码,CPU执行的是机器码,这个包含机器码的程序被称为可执行程序
先来看看源代码是如何变为可执行程序的
IDE是集成环境,很容易让初学者觉得源代码直接被CPU执行了
其实否则
源代码必须通过编译器编译 才能成为二进制的可执行程序
IDE里面集成了 编译器 调试器 ,C语言的编译器 主要有GNU编译器套件中的GCC、Microsoft C 或称 MS C、Borland Turbo C 或称 Turbo C
编译过程是一个复杂的过程,接下来聊聊这个复杂的过程
编译是个过程的总称,其中还包括不一样的阶段,源代码预处理阶段、编译优化阶段、汇编阶段、连接阶段
预处理器将对其中的伪指令(以# 开头的指令)和特殊符号进行处理,删除全部的注释,最后生成 .i文件
伪指令包括:
使用gcc命令能够输出.i文件
gcc -E helloWorld.cpp -o helloWorld.i
此时.i文件是删除了注释、宏替换、头文件也加载进来了,该文件比源代码文件大
内容太多,代码就不粘贴了,你们自行试验下
编译程序所要做的工做就是经过词法分析、语法分析、 语义分析,在确认全部的指令都符合语法规则以后,将其翻译成等价的中间代码或汇编代码
词法分析和语法分析千万不要混淆了,校招面试的时候被面试官给绕了半天
词法分析器识别出Token,把字符串转换成一个个Token
Token包括关键字、标识符、字面量、操做符、界符等
为何要这样作呢,把代码里的单词进行分类,编译器后面的阶段不就更好处理理解代码了嘛
语法分析阶段把Token串,转换成一个体现语法规则的树状数据结构,即抽象语法树AST
AST树反映了程序的语法结构
好比hello world代码通过语法分析以后会获得一个AST树
不少人疑惑为何要把程序转换成AST这么一颗树呢?
由于编译器不像人能直接理解语句的含义,AST树更有结构性,后续阶段能够针对这颗树作各类分析
语义分析顾名思义就是理解语义,也就是理解程序要作什么
好比理解 "+" 符号是执行加法、"="号是执行赋值操做、"for"结构就是去执行循环等等
那到底怎么理解呢?
这个阶段要作的就是进行上下文分析,上下文分析包括引用消解、类型分析以及检查等等
引用消解:找到变量所在的做用域,一个变量做用范围属于全局仍是局部做用域
类型识别:好比执行a=3,须要识别出变量a的类型,由于浮点数和整型执行不同,要执行不一样的运算方式
类型检查:好比 int b = 3,是否能够进行定义赋值,等号右边的表达式必须返回一个整型的数据或者可以自动转换成整型的数据,才可以对类型为整型的变量b进行赋值
通过语义分析后得到的信息(引用消解信息、类型信息),会在AST上进行标注,造成 带有标注的语法树,让编译器更好的理解程序的语义
在语法分析后有了程序的抽象语法树,在语义分析后有了 带有标注的AST 和符号表后,就能够深度优先遍历AST,而且一边遍历一边执行结点的语义规则
对于解释性语言整个遍历的过程就是执行代码的过程
解释性语言如Python 等,在遍历带有标注和符号表的抽象语法树便可开始执行
编译性语言须要生成目标代码,如C、C++
编译型语言须要生成目标代码,而解释性语言只须要解释器去执行语义就能够了
以前校招面试的时候,面试官看我把hello world讲的这么好,顺手问了句Java、Python 执行hello world的过程同样么?
当时愣了下,知道不同 可是没解释的很清晰
对于不一样架构的CPU,生成的汇编代码不一样,若是优化是针对每一种汇编代码,那这个过程就至关复杂了
因此在生成目标代码以前增长一个过程,先生成一个 中间代码IR,统一优化后再生成目标代码
优化代码主要从分为本地优化、全局优化、过程间优化
本地优化:可用表达式分析、活跃性分析
全局优化:基于控制流图CFG做优化
过程间优化:跨越函数的优化,多个函数间做优化
说了一些干的,举个例子让你们理解下到底如何优化
活跃性分析就是将一些没有用到的代码删除,好比一些没有用到的变量
目标代码生成就是将优化后的IR代码翻译为汇编代码
翻译为汇编代码主要步骤是
编译阶段使用的指令
gcc -S helloWorld.cpp -o helloWorld.s
生成的汇编代码:
用的GCC版本信息以下
上面的编译阶段的生成的汇编代码仍是人能看懂的,不是给机器直接执行的,机器执行的叫作机器码
机器码放在可执行文件中
unix环境中存在好几种目标文件:
不一样的操做系统的可执行文件格式不一样
汇编程序生成的其实是第一种类型的目标文件,连接完成以后才能生成可执行文件
将汇编阶段生成的一个个的目标文件连接在一块儿生成可执行文件
其实不少人不理解为何须要连接这个过程,明明汇编阶段已经生成目标代码
举个例子你们就明白了,平常作系统开发的时候,咱们讲究系统功能模块化 如今都是微服务
一个复杂系统,每每会分红多个不一样的子系统 子系统在拆分为不一样的功能模块
连接的过程也和这个相似 一个复杂的软件须要拆分为多个不一样的模块,每一个模块独立编译
根据须要在 "组合" 起来,这个组装模块的过程就是 连接
好比main函数中调用了printf函数,mian函数在编译时并不知道printf函数的地址(每一个模块都是单独编译的)
可是调用又必须知道函数地址才能发生调用关系
编译时暂时把这个地址搁置,连接时在进行地址修正
连接完成以后会造成一个可执行文件 ,可执行文件也叫ELF文件
这个ELF文件以及其余文件也够喝一壶,放在后面讲聊文件系统 一块儿聊
装载就是把可执行程序加载到内存中,供后续的CPU执行
在linux命令行中咱们常常这样执行一个可执行程序
./a.out
这样一下就把程序加载到内存中,加载完成以后直接执行了
其实你可使用
strace ./a.out
这个命令能够看到全部的系统调用
能够看到 第一个执行的系统调用是 execve
经过 man execve 能够看到这个函数的描述
execve() executes the program pointed to by filename. filename must be either a binary executable, or a script starting with a line of the form:
#! interpreter [optional-arg]
execve()执行文件指定的程序 文件必须是二进制可执行文件,或者执行一个以 shebang开头的脚本
Shebang 就是 #!
开头
经过查看Linux的execve源码以下
主要执行工做落在了 do_execve
上,继续看看 do_execve 源码
前面就是计算一些参数如argv、env 拷贝相关数据,最终装载程序执行search_binary_handler
list_for_each_entry
函数很是重要,这个函数遍历全部formats列表,找到当前系统合适的可装载格式
前面已经说过,linux 下可执行文件格式是ELF文件
retval = fmt->load_binary(bprm)
就是load可执行程序
load_binary是加载二进制文件啊,咱们的程序明明是ELF文件
仔细看看load_binary的源码会发现里面有一个初始化,初始化的时候会作一个赋值替换为
或许到这里你们基本已经了解了,但仍是疑惑怎么才能判断加载的ELF文件
能够去看看源码怎么写的 (源码太长,这里就不粘贴了 告诉你位置有兴趣的本身去看看)
源码位置:
有个函数叫 static int load_elf_binary(struct linux_binprm *bprm);
在 /fs/binfmt_elf.c Line 820
再看看咱们的可执行程序头上长啥样 readelf -l a.out
便可查看可执行文件头部信息
解释器经过判断 Program Headers 中的 INTERP 的值获得该可执行程序的文件类型
咱们的CPU执行程序的步骤是:
上面步骤是一个循环也称为CPU指令周期,CPU 的工做就是一个周期接着一个周期,周而复始。
更多关于CPU执行的问题,能够看看好朋友小林的 你很差奇 CPU 是如何执行任务的?
或者持续关注,后面我会更新关于CPU执行调度的文章
在Unix系统中,每一个进程都会默认打开三种标准I/O 分别是STDIN、STDOUT和STDERR
printf源码
这只是第一次源码,愿意了解的能够看看vfprintf实现,你会发现底层使用了 缓冲输出
输出是一次output,也就是会经历一次从内存外部文件系统的数据转移
到这里基本就讲完了了hello world所有内容,讲完了不必定是讲透彻了
好比 关于文件系统的知识、IO知识、CPU调度知识、进程管理、内存管理等等知识都无法经过一篇文章说透彻
说实话一个小小的hello world藏着大学问,囊括的内容也实在是太丰富了
今天只是从总体上把控了一下,细节内容后面写操做系统会一一更新
我是龙叔,咱们下期见