1.首先说C语言转汇编语言,全局变数的声明是编译器自动给变数分配好内存空间然后把数据存进去,还是让CPU一条指令一条指令的执行,把数据存入到内存中?

第二,C语言的局部变数声明是如何做的,比如我在main函数中,int t;不赋值的话,会有mov指令吗?编译器怎么给这个变数t分配空间呢?是也像全局变数一样,在运行之前就分配好空间还是使用mov指令,给这个局部变数分配空间呢?可是,这个mov的操作对象又是什么呢?我知道int t=1;mov的操作数是1,把1存入到内存中去,可是,不赋值是怎么样的呢?

第三,关于数组的全局变数和局部变数的声明,编译器是怎么样做的呢?汇编代码是怎么样的呢?

第四,像面向对象的高级语言,当new一个对象的时候,汇编语言是怎么样的呢?不可能把这个类的内容,全部使用mov指令,复制到新的内存里面去吧?那这样要浪费多少时间啊?要是说,只mov类的地址,那也肯定是不可能的,因为每个new的类,都是独一无二的,不可能共用同一个地址的。

第五,就比如,int a=1;这句代码,如果是放在函数内部写,就是局部变数的话,转为汇编语言就是mov [地址],#1,就是说把1写入到内存地址为多少的内存里去,但是,如果是全局变数的话,转为汇编就是没有mov什么指令了,就是在数据段里面声明了一个变数,我感觉相当于运行程序后,自动把变数分配了一个全局变数空间,并且把值写进去,不需要指令去把1写入到内存了。只是编译器自动执行的。


偶然看到这个问题,而且看到题主还在更新就进来答一下。

这个问题太庞大了……标题的问题其实跟题目描述关系不大,如何转换更多的是一些工程上的问题,比如说语法分析、编译器前后端、llvm中间码之类的东西,不太涉及到细节,就像你建房不会从烧砖开始一样。而问题的描述是一些很细节的问题,应该这才是重点,就回答这些吧。

问题1:

问题描述的不是很清楚,说编译器如何如何,又说cpu指令如何如何,这其实八竿子打不著,前者是你构建程序的时候的事情(比如说生成exe),后者是这个你构建出来的(exe)程序运行时可能会发生的事情,这是不一样的。

我就从头说说从你写出来的代码到可运行的程序都发生了什么,这流水账中与全局变数有关的节选吧。当然,这份流水账很不详细很不工程化,但是回答这个问题差不多够了。

  • 首先你在程序里面写了两个全局变数int a=10;int b=0;,然后在某个子程序里代码访问了他a+=3;b+=5;
  • 然后你编译了这个程序。编译器会来来回回看几遍你的代码,找出所有的全局变数记录下来。
  • 编译器把所有找到的全局变数整合到一起,无论是你的还是你调用的其他库里的,都挨著放一起。
  • 然后把他们的名字用他们的位置替代,比如说a是第一个就写0,b是第二个就写4(假定int是4位元组),因为a是int占用了0/1/2/3这4个位置。以此类推。
  • 然后光有了他们本身的位置还不行,编译器根据目标系统以及其他参数等整理出一个可用的内存起始地址,比如说0x00403000,把这些数字再加上这个数字。
  • 当然,调用这些变数的地方也都改变了,比如说b+=5也会变成 [0x00403004]+=5
  • 最终编译器就把这些处理后的变数整理到了目标文件中,顺便把地址0x00403000也写进去了。
  • 你运行了这个生成的文件。操作系统的程序载入器查看文件中要求的各种环境,就看到某处写著「我需要在0x00403000处存储大小为n的数据,帮我申请到,然后把旁边这堆数据载入到那里。
  • 在处理了这些环境之后,程序载入器把控制器给到你自己写的main函数,此时这些变数当然已经载入完成了。

大致就是这样,所以其实这些全局变数被放到了一起存储在生成的目标文件里,在你启动这个程序时,操作系统的程序载入器把这一段数据读到对应的内存处放著,就跟你写代码读个文件到内存里什么的没多大区别。

问题2:

说完了全局变数,这个问题好像是局部变数。

一句话来说的话,就是这些局部变数在各子程序的栈帧中,子程序内部扩充栈空间存储这些局部变数,并在离开子程序时随著栈帧被回收。这跟mov没什么关系,大致就是由sp寄存器管理著的一个「栈」,通过调整栈的大小来容纳这些局部变数,通常来说你会看到sub sp,xxx就是在开局部变数。

至于栈的细节……有点长,如果已知就跳过吧。

栈这个东西……,虽然一般口语都说堆栈,当然堆和栈是两个东西。栈是什么呢,抽象的说,栈是一个只有一个口的容器,先放进去的东西会被压在下面拿不出来,只能后放进去的拿出来了,之前放进去的才能拿出来,就像一个薯片桶一样。不过里面的东西虽然拿不出来,但是可以看到,也可以对里面的东西进行内容上的修改。系统给每个程序(准确的说是线程)配了这样一个东西,当然实际上就是一段内存模拟出来的,不过谈论这个话题的时候先把什么内存啊自己啊这些细节的东西放一放,只想抽象的部分

这个栈是给程序保存流程(比如说哪个子程序调用了哪个子程序)和一些小的局部变数用的。使用规则是这样的。

  • 当调用一个子程序的时候,把参数扔进栈里,然后把返回地址扔进栈里,返回地址是什么呢,就是存著你调用完后回到上一层,回到上一层的哪一行呢,存著这个行号。做完之后把控制权给这个子程序。
  • 当离开一个子程序时,从栈里一件一件往外取东西并扔掉,直到找到返回地址,记下来,然后再看看自己有几个参数,再把对应数量的东西拿出来扔了。把控制权交给返回地址那边。
  • 子程序执行的时候,可以在栈里随时放几个盒子存东西,反正要离开子程序时都会扔掉。

大致就是这样的,举个例子演示一下实际上栈的工作流程,这部分有点枯燥,不知道怎么能写的明白一点。

foo(){
bar(5,10);//我们假定从这里开始执行
printf(...);
...
}
bar(int aa,int bb){
int x;
int y=100;
x=5;
baz();
return;
}
baz(){
int pi=3;
...
return;
}

  • 程序执行到了一句bar(5,10), 按照上面的规则,先5扔进栈,再10扔进堆栈,然后把回来的地方也就是"返回地址-foo的第二行"扔进堆栈。最后把控制权交给了子程序bar。
  • 此时堆栈内容为["返回地址-foo的第二行",10,5],从左到右看,左边是入口。
  • =========================
  • 程序执行到了bar的第一行int x,发现了一个没初始化的局部变数, 所以往堆栈里扔了一个空盒子
  • 此时堆栈内容为["盖上写著x的空盒子","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序执行到了bar的第二行int y=100往堆栈里扔了一个盒盖写著y里面装著100的盒子
  • 此时堆栈内容为["盖上写著y的盒子 内容物为100","盖上写著x的空盒子","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序执行到了bar的第三行x=5找到堆栈里标著x的盒子,把5扔进去。
  • 此时堆栈内容为["盖上写著y的盒子 内容物为100","盖上写著x的盒子 内容物为5","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序执行到了bar的第四行baz(),这又是一个子程序调用,不过没有参数,所以直接把回来的地方扔进去就行了。 然后控制权交给baz。
  • 此时堆栈内容为["返回地址-bar的第五行","盖上写著y的盒子 内容物为100","盖上写著x的盒子 内容物为5","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序执行到了baz的第一行int pi=3,照常扔进去一个装著3写著pi的盒子
  • 此时堆栈内容为["盖上写著pi的盒子 内容物为3","返回地址-bar的第五行","盖上写著y的盒子 内容物为100","盖上写著x的盒子 内容物为5","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序执行到了baz的最后一行return,该离开子程序了,从堆栈里取出一项,发现是没用的pi,扔掉,然后再取出一项发现是返回地址,记下来,然后看看自己好像没有参数,不用再取了。然后细看返回地址写著bar的第五行,于是把控制权交过去。
  • 此时堆栈内容为["盖上写著y的盒子 内容物为100","盖上写著x的盒子 内容物为5","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序执行到了bar的第五行return,又是一个返回,继续离开当前子程序。从栈里拿出一项,发现上面写著y,扔掉。再从栈里拿出来一项,发现上面写著x,扔掉。再从栈里拿出来一项,发现是返回地址foo的第二行,记下来。然后看著自己有两个参数,于是再拿出来两项扔掉。 控制权交给foo的第二行
  • 此时栈空。
  • =========================
  • 程序执行到了foo的第二行printf(...)下略。

栈就是这样参与到程序的流程中的,可以让程序多层级的调用变得很明确,是一个很好的数据结构。

而汇编层面上,栈是用sp寄存器模拟的,sp寄存器里存储的内存地址就代表著栈的顶端,如果要往里存东西,比如说存个int,要把栈顶往上挪4位元组存储,就把sp减少4,然后把这个int写到对应的地址就行了,要取出的话就反之,读取sp中地址的内容,然后再把sp加上4,栈就算复位了。所以不赋值的局部变数开辟,就是直接把sp减少对应的大小就不管了。

另外大部分架构还会用bp寄存器存储一些信息辅助查找返回地址和参数,不用真正的一个个看过去。

问题3:

如果你理解了前两个问题,数组的变数声明也就没什么难点了。全局变数的话同样是存储在文件中那堆全局变数里,只不过会有点长而已,载入的时候还是所有全局变数作为一块数据载入内存。局部变数的话也仍然是sub sp,xx,你经常可以看到一些非常大的数字比如说sub sp,0x3200,这通常都是一些数组或者大的结构体。经常说不要局部变数放很大的东西也是因为sp指向的这块内存终究是有限的,虽然其实不小。

问题4:

既然你都说了只复制地址是不行的,那当然是全部复制的啦,不过所谓的全部其实也没多少。首先是类里面的非静态成员变数们,这些肯定是要复制一份或者说给他们开新的内存的,然后呢,其实就没有然后了,其中的方法或者说子程序都是固定代码,这是不用复制的。所谓的独一无二其实也就是那些数据部分是独一无二的。

问题5:

这个没看出来问什么,不过提醒下,编译器只管生成出目标文件,执行这个文件什么的已经与编译器无关了。另外局部变数的int a=1一般还是要两句代码的,比如说sub sp,4然后mov [sp],1。不过实际应用上还会用到bp来优化,只用指向栈顶的sp有时候会不方便。


差不多也就这样,上面这些回答里面省略了大量的细节,还有一些为了简化理解的小妥协,跟实际应用有一定差距,不过拿来理解思路应该差不多,但不要认为工程上的东西就是这么做的。


你要明确:

  • 编译时和运行时。
  • 语言特性的spec与实现。
  • 编译器优化。

很多详细的事情你应当看书,比如C专家编程、C pitfalls。

对于1,「编译器自动给变数分配好内存空间」和「让CPU一条指令一条指令的执行,把数据存入到内存中」并不矛盾。

具体来讲,全局空间是在程序启动时分配的,但这个空间的初始化(如果有)有可能是需要被执行的。

2:

int t;不赋值的话,会有mov指令吗?

如果你真的没用这个变数,那它很可能会被优化掉,在编译结果里根本不会存在。

编译器怎么给这个变数t分配空间呢?

通常来讲函数内的局部变数在栈上。整个栈空间是在程序/线程创建的时候预先分配好的,进入一个函数的时候,在这个分配好的栈空间里使用某一块。所有对这个变数的访问,会变成「当前栈+固定偏移量」的形式。

可是,这个mov的操作对象又是什么呢?

可以是CPU寄存器。

我知道int t=1;mov的操作数是1,把1存入到内存中去,可是,不赋值是怎么样的呢?

这里有几个要点:

  • 编程语言的语句与其编译结果并不一一对应,特别是对于编译到native code的语言,和开了高级别优化选项的时候。很可能一个语句什么都不干,或者一个语句干了很多事情。具体到这件事情:
    • int t可能存在在栈上,也可能被优化掉,变成一系列对寄存器的访问,也可能完全没有。
  • 由于1是一个非常小的操作数,它很可能变成CPU指令里的「立即数」。
  • 不赋值那就是什么都不干。但它依然会造成影响:
    • 如果这个变数没有被优化掉,那它的存在会影响这个函数的栈尺寸的计算。
    • 允许你后续操作访问这个变数。具体怎么访问,取决于它会在栈上,还是被优化掉了。

3:

数组的实现没有任何特别的地方,也是占用一定尺寸的空间而已。

4:

对于C++这种非托管的语言,new的时候会干两件事情:

  • 在堆上分配对象尺寸那么大的一块内存(我们先不考虑自定义分配器);
  • 使用这块分配好的内存执行构造函数。

不可能把这个类的内容,全部使用mov指令,复制到新的内存里面去吧?那这样要浪费多少时间啊?

构造函数里面干了什么,完全取决于构造函数写了什么。

另外,构造函数没有任何神奇的地方。本质上和这样的C函数没有区别:

typedef struct Billy
{
int age;
const char* name;
};

void Billy_constructor(Billy* this)
{
this-&>age = 65535;
this-&>name = "Billy Herrington";
}

5:

如果是函数局部变数,不优化有可能是「将内容1放在内存栈顶+a的四位元组处」,优化有可能是什么都不做。

基本类型的全局变数在程序映像载入时就完成了初始化,基本上就是把可执行文件直接拷贝到内存里,可执行文件在链接时已经搞好了全局区各个位置的值,所以载入完了就已经好了。


你开的这个话题太大了,而且目测你对这部分内容连最基础的了解都没有。所以,你如果真的有兴趣的话,还是建议你系统的学一下编译原理相关的内容。

这么东一榔头西一棒子,往往是你看的时候觉得懂了,但真要上手做点什么的时候,就会发现毫无头绪了。

学而不思则罔,思而不学则殆


编译器究竟为何要下此狠手?年近6旬的操作系统究竟如何管理内存?局部变数来去无踪,它又究竟是如何分配的?这一切的一切,是人性的泯灭、还是道德的沦丧?

敬请收看新一期 Pearson 在线《深入理解计算机系统》,Randal David 为您讲述计算机背后不为人知的秘密!


首先说明,本回答为个人理解,有错请指出,谢谢。

其实你问的这些问题你自己写一遍然后看汇编就可以了

1.直接把数据段映射到内存,编译器已经把初始值写到了数据段。不过类的还要在main前执行下构造函数,这个编译器不能帮你。

2.局部变数声明是通过sub esp在栈上预留空间,如果有初始化当然是mov,不初始化就什么也不干,变数值是随机的。

3.数组没什么不一样,只是局部数组变数初始化好像是一个一个mov过去的,因为没保存在数据段里。

4.先分配空间初始化再执行构造函数。

5.同1

还有,编译完之后就与编译器没有关系了。


推荐阅读:
相关文章