进入编译器后,一个函数经历了什么?

来源 | 编程技术宇宙

责编 | Carol

封图 | CSDN 付费下载自视觉中国

我是一个函数

我是一个函数,名叫str_upper,我可以把输入的字符串从小写变成大写。不信你看,我长这样:

char* str_upper(char* str, int len) {
  
  char upper[256];
  
  if (len >= 256 || len <= 0) 
    return nullptr;

  for (int i = 0; i < len; i++) {
    if (str[i] >= 'a' && str[i] <= 'z') {
      upper[i] = str[i] - 32;
    } else {
      upper[i] = str[i];
    }
  }
  
  return upper;
}

上面是我的源代码形式,听我的好朋友str_lower说,一会儿我们就要一起被送到一个叫编译器的地方加工处理了,我心里害怕极了。

编译器之旅

没多久,我们就来到了这里,一座很庞大到高楼,里面有好多精密的机器在不停的运转着。

一进入大厅,好多函数代码在这里排队等待。

我抬头向上望去,不知道有多少层楼,每一层都有一个指示牌,从下往上分别写着:

  • 预处理

  • 词法分析

  • 语法分析

  • 语义分析

  • ···

再往上太远就看不太清楚了。

所有的函数代码按照文件为单位排好队,静静地等待着。

不过没有等太久,就轮到了我们这一队。

来了一个工作人员把我们带到了一个房间,让我们都好好躺着,一台机器快速的从头到尾扫描了一遍,将我们所在文件中出现的#include#define全部给替换掉了。

接着,通过房间里的电梯,将我们送上了二楼。

接下来的一段时间,我们在好几层楼都做了“体检”,每个函数都被那些像CT一样的机器照了个遍。

不一会儿,来到了编译层,这一层有一个特别奇怪的机器,我看到一个个函数被送了进去,出来的时候都变了样子。不仅如此,接待处的工作人员看起来很凶,我这下更加紧张了。

函数调用约定

工作人员拿到了我的资料,瞅了几眼,问到:“请问你的调用约定是什么?”

我有些懵,不太懂他的意思,小声问到:“不好意思,你刚问什么?”

工作人员有点不耐烦了,提高了音量,“我是问你调用约定是什么?调用约定啊!”

看见我仍然一脸茫然,工作人员直接给我的资料上调用约定那一栏盖上了一个标记:cdecl

我有点摸不着头脑,同行的小伙伴str_lower拽了我一下说到:“他是在问你函数的调用约定,就是约定调用函数的方式,涉及怎么传递参数,谁来恢复调用栈等”

他这一说我才反映过来,“这个调用约定都有哪些可选的呢?”

“一般有三种:”

  • cdcel,参数从右往左入栈,主调函数负责恢复栈平衡

  • stdcall,参数从右往左入栈,被调函数负责恢复栈平衡

  • fastcall,参数通过寄存器传递,寄存器不够再用栈传递

“他刚才看你没有显式声明,就默认给你cdecl的方式了”,小伙伴继续说到。

我点了点头,原来调用个函数还有这么多讲究呐!

Stack Canary

“别闲聊了,快进去吧!”,工作人员催我了。

我准备走向那台可怕的机器。

“唉,等一下”,正紧张着,工作人员又叫住了我。

我回头看去,工作人员正招手让我过去。

“你好,是我的代码有什么问题吗?”,我紧张的问到,生怕有错误被打回去,连累我们整个文件都要被遣返。

“不是,是我注意到你的函数里有一个局部数组,需要给你加一下栈溢出保护”,工作人员说到。

我看了下我的代码,确实有一个局部字符数组:

char upper[256];

“栈溢出保护是什么啊?”,我小声问到。

工作人员没有搭理我,忙着给我的资料上加东西。

旁边的小伙伴又把我拽了过去,说到:“咱们函数里面定义的局部变量、参数是存放在线程栈里面的。线程要不断游走在不同的函数中,调用函数后为了能回到原来的地方,调用之前把返回地址也放在了线程栈里。就像这样,你看会不会有什么问题:”

我仔细看了下,“哦,要是越界访问我的upper数组,那就可以修改返回地址,那可就危险了!”

“很聪明嘛!”

“那这个怎么加保护呢?”,我问到。

“你看,函数进来之前,先在局部变量和返回地址之间设置一个数值,函数返回之前再去检查一下,如果栈里的数据被破坏了,检查这个数值就能发现,提前抛出异常!”,小伙伴耐心的解释到。

“这样啊,那岂不是要把我打回去加上你说的这些设置和检查代码?”,我继续提问。

这时,工作人员听到了我们的闲聊,“不用,我们编译器自动添加好了,快去吧,已经处理好了”

我瞥了一眼,看到我的资料上增加了一个叫Stack Canary的标记。

我小心翼翼的走进了那架奇怪的机器,立刻就失去了知觉,等我醒来时,我的身体已经发生了变化,变成了一堆奇怪的代码,现在我长这样了:

链接

没过一会儿,我们这一队的所有函数代码都编译完成,大家从原来的.c文件都搬到了新家:一个.o文件,我也再次见到了小伙伴str_lower。

“咱们是不是已经完成了编译,可以离开这里了吧?”

“还不行,编译虽然是完成了,还差链接这一步呢!”

又过了一小会儿,和我们一起过来的其他文件的函数代码也编译完成了,咱们一堆.o文件一起被送到了编译器大厦的顶楼:链接层。

这一层也有一个巨大的机器,机器背后连接了一个管道,不知通向了哪里。

我们这一批的所有.o文件挨个走进了这个巨大的机器,像是一条时空隧道一般,穿行于其间,我感觉到了巨大的压力把我们挤压在了一起,很快我们再一次失去了意识。

醒来之后,我发现所有的函数们都被合在了一个文件中,这是一个可执行文件,而我的身体也再次发生了变化,变成了一段段的二进制指令,现在我长这样了:

终于离开了编译器,真是一趟难忘的旅程,不过我再也不想来了······

彩蛋

没想到命运跟我开了一个玩笑,我的第一次运行就出了错!

我又要被打回去重新改造,再走一遍这魔鬼般的旅程。

你能帮我看看,我的代码哪里有错吗?

更多阅读推荐

### 回答1: 程序从源代码开始,经历了以下四个地址空间: 1. 符号地址空间:程序员使用的地址空间,其中所有的变量和函数都用符号代表,例如变量 a 和函数 foo()。 2. 目的地址空间:也称为逻辑地址空间,处理器使用的地址空间,其中所有的变量和函数都使用逻辑地址。逻辑地址由程序计数器和偏移量构成。 3. 统一目标地址空间:链接器使用的地址空间,其中所有的变量和函数都使用统一的地址表示,链接器负责将目标文件中的变量和函数映射到统一地址空间。 4. 物理地址空间:最终的物理内存空间,运行时处理器将逻辑地址转换为物理地址,从而在物理内存上执行程序。 ### 回答2: 当程序从源代码开始执行时,它需要经历以下几个阶段来转化为实际执行的物理地址: 1. 符号地址空间(Symbol Address Space):源代码在编译过程中首先被转换为符号地址空间。在这个阶段,编译器将源代码中的标识符(变量、函数等)转换为对应的符号,而不考虑其实际存储位置。这个阶段的输出是一个符号表,记录了每个符号及其对应的符号地址。 2. 目标地址空间(Target Address Space):在链接过程中,编译器会将源代码与其他模块的目标代码进行合并,创建一个统一的目标地址空间。在这个阶段,编译器会将符号地址转换为相对地址,并为每个目标模块分配起始地址。这个阶段的输出是一个目标文件,其中包含了相对地址。 3. 统一目标地址空间(Unified Target Address Space):在加载过程中,操作系统将目标文件加载到内存中,并为每个进程分配虚拟地址空间。在这个阶段,操作系统会将相对地址转换为虚拟地址,并为每个虚拟页面分配一个唯一的虚拟页号。这个阶段的输出是一个虚拟地址空间。 4. 物理地址空间(Physical Address Space):在内存访问过程中,虚拟地址需要转换为物理地址。当程序运行时,CPU会根据虚拟地址的页表映射关系,将虚拟页号转换为物理页号,并通过偏移量计算出最终物理地址。这个阶段的输出是一个物理地址空间,用于访问实际的内存位置。 总结起来,程序从源代码开始,经历了符号地址空间、目标地址空间、统一目标地址空间和物理地址空间这四个阶段的转换过程,最终将源代码中的符号转化为实际的物理地址,实现了计算机程序的执行。 ### 回答3: 从源代码开始,程序首先经历了符号地址空间阶段。在这个阶段,源代码中的所有标识符(例如变量、函数等)都被编译器转换成符号地址,这些地址是编译时使用的虚拟地址,不具体对应物理存储空间。 接下来,程序进入目的地址空间阶段。在这个阶段,链接器将所有的目标文件以及库文件合并,解析各个模块之间的符号引用关系,并分配各个符号对应的目的地址。目的地址空间是一种逻辑上的地址空间,用于指定各个符号在可执行文件中的相对位置。 然后,程序进入统一目标地址空间阶段。在这个阶段,将目标地址空间中的地址映射到统一目标地址空间中的绝对物理地址。这一步骤是为了解决内存地址不连续带来的问题,使得程序的部分地址可以被映射到物理内存的不同位置。 最后,程序经过链接器和加载器的处理,进入物理地址空间阶段。在这个阶段,操作系统将程序的物理地址空间从磁盘加载到主存中,并为程序分配实际的物理内存地址。此时,程序的代码、数据和堆栈等部分被映射到具体的物理存储空间,可以被处理器执行。物理地址空间是系统实际的地址空间,与硬件的物理存储器对应。 通过以上阶段的转换和映射,程序从源代码开始的虚拟符号地址最终转化为了在物理内存中的实际物理地址,使得程序能够被正确加载和执行。
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值