C语言解释器Java版-1-内存分配

来源:互联网 发布:facebook总是网络错误 编辑:程序博客网 时间:2024/05/29 14:33

1 内存分配

基本了解了内存模型后,接下来考虑在实现解释器时如何进行内存分配,即怎样将每个内存区进行数据分配和释放。
在进行具体内存分配前,需要定义输入接口:被解释的源程序 file.c 和入口函数 func,然后根据输入生成对应的AST、控制流程图和符号表。有了这些原始数据后,开始进行内存分配。

1.1 全局数据分配

这里的全局数据包括如下几类,每一类的分配都不尽相同。主要操作的内存区是代码段区和数据区。

1.1.1 函数内存分配

被解释程序file.c中的所有已定义函数,不包含extern的外部函数和 没有声明定义的函数。函数对应的内存段区是代码段区。在标准的C语言内存分配中,代码段区存放的是函数对应的二进制代码。
在我们的实现中,每一个函数在代码段区分配一个四字节的内存空间,内存地址递增。由于每个函数占用的内存大小相同,都是4个字节,所以这里不需要考虑内存对齐问题。(代码段区起始地址应该能被4整除)

1.1.2 字符串常量

在C语言中,字符串常量一般是一个char*的定义:

  • 在分配内存时无法从符号表中通过符号类型获取长度;
  • 为了统一处理指针类型(使用四个字节地址表示),char*被分配为一个四字节的内存地址,char*的值则分配到一块固定内存范围(即字符串常量区);
  • 这样分配的另一个好处是,对于同样的字符串,只需要从指定的内存地址获取字符串的值即可,不需要另外开辟一段空间去存放字符串的值,能够节省空间。

另外,字符串常量区不需要考虑内存对齐,每一个字符都是一个char,所有不用考虑内存对齐问题。

1.1.3 全局定义变量

包含初始化和未初始化的变量。虽然在标准的定义中,区分了初始化和未初始化,但是,为了实现简便,我们的实现中做了合并处理。

  • 对于每一个非指针类型的全局变量,按照大小分配指定的空间即可;
  • 对于类型为指针的全局变量,需要分配的是4字节的地址空间,对于指向的内容,会在使用的时候进行指向处理。
  • 每个全局变量占用的内存并不相同,所以,在进行内存分配时,需要考虑内存对齐。但是,在查阅相关文档时,并未获得有关全局变量在分配时如何进行内存对齐处理。所以,我们采用的方案是:分配的内存起始地址满足当前变量的对齐要求即可。

1.1.4 静态变量

包含全局定义和局部定义的静态变量。

  • 静态变量的地址分配和全局变量没有区别;
  • 在全局数据分配时,需要分配局部静态变量,所以需要遍历每一个函数,并找到该函数中定义的局部静态变量,并对其分配内存;
  • 局部静态变量是需要关注初始化问题的。对于一个局部静态变量,第一次调用变量所属函数和第二次调用的值是不同的。有如下代码,第一次调用时sv的值是1,第二次调用时sv的值就是2….所以在第二次调用时,就需要注意,sv的赋值语句已经不起作用了。
void f(){    static int sv = 1;    printf("%d",sv);    sv++;}

1.2 局部数据分配

局部数据分配,主要操作的内存区是栈区和堆区。

  • 在进入函数时,需要立即进行局部变量的内存分配。分配的局部变量包括:函数形参、每一个作用域的局部变量。
  • 内存分配原则是,先参数,后变量,参数从右向左,变量按定义顺序。如下所示代码,分配内存的顺序是:b->a->c1->d1->c2->d2。
  • 需要注意,在一进入函数f后,就需要将c2和d2的内存进行分配。这样对于永远不可能走到的分支,可能会无效分配很多内存,但是这是VC、GCC等编译器的共同选择。这样做可以解决的一个问题是,函数中的跳转语句或者其他循环语句,只分配一次内存,且可以解决因为这些语句引起的找不到内存、内存未分配等问题。
int f(int a,int b){    int c1,d1;    //....    if(c1 + c2 > 0){        int c2,d2;        //....    }    return 1;}
  • 进入函数时分配内存,意味着,退出函数时,需要释放内存。由于这些内存都是在栈中分配的,所以进入函数时,需要创建一个新的栈空间,存放函数中的局部变量,退出函数时,退出该栈空间(栈帧,stackFrame)即可。
  • 栈区的内存分配是从高地址向地址进行分配的,也就是b比a的地址高。但是要注意,从高到低分配并不意味着起始地址就低。对于变量b,如果起始地址是0x70000004,则b的最大地址位置是0x70000008,而不是0x70000000,a的起始地址是0x70000000,最大地址是0x70000004。
  • 堆区的内存分配是从低地址向高地址进行的。但是堆区的地址分配释放策略有很多种,不同的策略会导致分配的地址差异。对于堆内存的释放和分配策略,这里不多做说明,如果后续有需要,会单独进行说明。

1.3 函数调用

在解释执行函数调用语句时,需要先保存当前栈空间,然后切换到新的函数,创建新的栈空间,当新函数执行完成后,退出新的栈空间,进行栈还原,并执行下一条语句。这里,将每一个函数的一个栈空间称为一个栈帧(stackFrame)。函数调用的栈帧组成如下图所示:
函数调用栈帧

  • 每个frame由局部变量和参数、返回地址构成。在图中,main函数调用f1函数时,main的stackframe中保存了f1函数的实参,而f1函数的stackFrame中保存了main函数下一条语句的地址,也就是f1函数的返回地址。
  • 当main函数开始调用f1时,先将调用的实参放入到mainframe,创建新的f1 stackFrame,将main函数调用完成f1后的下一条语句地址放入f1 stackFrame中。
  • f1 stackFrame的其他创建工作还包括局部变量的分配等。在实际解释执行时,内存分配的同时,需要对参数进行赋值。这时候会读取mainframe中的实参值对f1的形参赋值。需要注意,这里的赋值也可能发生隐式类型转化。
  • 函数调用的另外一个问题是可变参数的问题。在我们目前实现中,对于可变参数是直接忽略掉了。如果后续有需求,对可变参数会重新进行说明。
0 0
原创粉丝点击