实现简易的C语言编译器(part 10)

        前面我们已经详细分析并实现了简易C语言的前处理、词法分析、语法分析和语义分析过程,最终得到了一棵没有语法错误、节点相互关系清晰的抽象语法树。这一部分,我们将继续遍历这棵树,生成汇编语言代码。
        在进入这部分之前,有必要强调一下:出于简化的目的,我们这里定义的抽象语法树可能和教科书不甚相同。所以,大家可以看一看前面部分的内容,关键是记住这棵语法树的结构,便于接下来在遍历语法树时,定义节点函数和相关处理有清晰的认识。
        除了一些对编译器实现相关的内容需要单独研究,我这里不打算详细介绍汇编代码其余的知识,这部分的内容完全可以上网或者从相关的书籍中获取。唯一说明的是,我们将采用AT&T格式的汇编代码,与之相对的是Intel格式。两者格式看似相差很大,但是主要的操作指令或者思路基本上是完全相同的。使用AT&T格式,在我看来,更有助于理解汇编指令操作过程,这是唯一的原因。
        为了跟上时代的趋势,我们将实现自动生成64位的汇编代码,或者俗称x86-64汇编代码的过程。与传统的x86格式的相比,操作指令增加了对四字的扩展,指针类型从4个字节变成了8个字节;同时,新增加了若干寄存器,对函数参数有了支持。具体的差异可以参见官方文档,但可能需要科学上网。
        生成汇编代码的过程相对来说稍显复杂,我们分成多个部分进行分析。

7.1 操作数

        大多数操作指令都有一个或者多个操作数,指示出执行一个操作中要引用的源数据值,以及存放结果的目标位置。按照操作数的类别,可以分为:寄存器,寄存器和存储器引用。

7.1.1 寄存器

        寄存器了表明某个寄存器的内容,AT&T格式的寄存器都是以%作为前缀的。先贴上一张主要的寄存器图:

图7-1 通用寄存器

这张图给出了我们将要使用的寄存器的功能和不同数据格式对应的寄存器名字,还有几个寄存器没有列出来,但仍然可以直接使用。C语言中,不同数据类型由于表示的字节长度不同,存储方式也不相同。例如,单字节的char,4字节的int和8字节的指针类型。具体表现,就是对应的操作指令的后缀不相同;此外,我们也不需要为int类型的变量动用%rax这样的表示形式,转而应该使用%eax
        为了表示这些寄存器以及得到变量类型对应的寄存器形态,定义如下的类:

class RegisterFrame:
    byte_base_regs = ['%rax', '%rbx', '%rcx', '%rdx']
    byte_index_regs = ['%rsi', '%rdi']
    byte_extend_regs = ['%r8', '%r9', '%r10', '%r11']
    stack_regs = ['%rbp', '%rsp']

    def __init__(self, name):
        self.name = name

    def to_str(self, type_str):
        if type_str == 'char':
            output_str = self.reg_to_8()
        elif type_str == 'int':
            output_str = self.reg_to_32()
        else:
            output_str = self.name
        
        if not output_str[0] == '%':
            output_str = '%' + output_str

        return output_str

    def reg_to_8(self):
        if self.name in self.byte_base_regs:
            return self.name[2] + 'l'
        elif self.name in self.byte_index_regs:
            return self.name[2] + 'il'
        elif self.name in self.byte_extend_regs:
            return self.name[1:] + 'b'
        else:
            raise Exception(f"Register {self.name} is not byte-compatible!")

    def reg_to_32(self):
        if self.name in self.byte_base_regs or self.name in self.byte_index_regs:
            return 'e' + self.name[2:]
        elif self.name in self.byte_extend_regs:
            return 'r' + self.name[2:] + 'd'
        else:
            raise Exception(f"Register {self.name} is not byte-compatible!")

通过传入64位寄存器的形态,我们将这12个寄存器按照名字的特点分成了4类,便于进行低位(8和32位)的转换。那么,这些寄存器用类表示,就是:

# the known register
Rax = RegisterFrame('%rax')
Rbx = RegisterFrame('%rbx')
Rcx = RegisterFrame('%rcx')
Rdx = RegisterFrame('%rdx')
Rsi = RegisterFrame('%rsi')
Rdi = RegisterFrame('%rdi')
R8  = RegisterFrame('%r8')
R9  = RegisterFrame('%r9')
R10 = RegisterFrame('%r10')
R11 = RegisterFrame('%r11')
Rbp = RegisterFrame('%rbp')
Rsp = RegisterFrame('%rsp')
# argument registers
Arg_regs = [Rdi, Rsi, Rdx, Rcx, R8, R9]

接下来,通过可以形如Rax.to_str('int')这种调用方式,就可以得到对应数据类型的寄存器形态。

7.1.2 立即数

        立即数就是可以直接放在指令后面进行操作的整数,如movl $8, %eax中的$8。同样地,我们也可以定义一个立即数类来管理:

class ImmediateFreme:
    def __init__(self, name):
        self.name = name

7.1.3 存储器引用

        存储器引用会根据计算出来的地址访问某个存储器位置。在AT&T格式的汇编代码中,通过括号来表示,如4(%eax)。具体的类可以定义为:

class MemoryFrame:
    def __init__(self, name, offset):
        self.name = name
        self.offset = offset

    def to_str(self, _type):
        if self.offset == 0:
            return self.name

        return str(self.offset) + self.name

其中,offset就表示存储器偏移。

7.1.4 操作数管理

        有了这些基本结构之后,我们定义一个类来管理这些操作数。

class FrameManager:
    def __init__(self):
        # A list of all registers
        self.all_regs = [Rax, Rbx, Rcx, Rdx, Rsi, Rdi, R8, R9, R10, R11]

        # A list of the registers currently free.
        self.regs_free = self.all_regs[:]

        # A list of all the registers that are "almost" free
        self.regs_almost_free = []

        self.stack = []

    def get_free_reg(self, preferred_reg=None):
        if len(self.regs_free) > 0:
            reg = None
            if preferred_reg is not None and preferred_reg in self.regs_free:
                reg = preferred_reg
            else:
                for item in self.regs_free:
                    reg = item
                    break

            if reg is not None:
                self.regs_free.remove(reg)
                return reg

    def push(self, preferred_reg=None, is_reg=True):
        if is_reg:
            reg = self.get_free_reg(preferred_reg)
        else:
            reg = preferred_reg

        self.stack.append(reg)
        return reg

    def pop(self):
        if self.is_empty():
            comment = f"stack is empty!"
            raise CompilerError(comment)

        loc = self.stack.pop()
        if not loc.is_register():
            return loc

        if loc in self.all_regs:
            self.regs_almost_free.append(loc)
            return loc

    def is_empty(self):
        return len(self.stack) == 0

    def done(self):
        self.regs_free.extend(self.regs_almost_free)
        self.regs_almost_free = []

这里我们使用了栈的结构来管理这些操作数,相比于采用其它的结构,它们将会在后面定义节点函数生成汇编代码时体会其优越性。值得注意的是,在弹出栈顶操作数的时候,我们并没有将其直接加入regs_free,而是先加入到regs_almost_free,直到调用done函数时,才加入到regs_free,我们也会在后面介绍这样做的原因。

7.2 操作指令

        顾名思义,作用于具体寄存器、立即数或者存储器的运算符。通常,我们会在指令后面增加一个字符后缀,以表明操作数的大小。同样地,可以采用寄存器的形式来封装我们将要用到的操作指令:

class OperationFrame:
    suffix_str = {'char': 'b', 'int': 'l', 'pointer': 'q'}

    def __init__(self, name):
        self.name = name

    def to_str(self, type_str):
        if type_str not in self.suffix_str.keys():
            type_str = 'pointer'

        output_str = self.suffix_str[type_str]
        if output_str is not None:
            output_str = self.name + output_str
        else:
            raise NotImplementedError('unexpected type')

        return output_str

那么对应地,操作指令可以封装为:

Mov = OperationFrame('mov')

Add = OperationFrame('add')
Sub = OperationFrame('sub')
Mul = OperationFrame('imul')
Div = OperationFrame('idiv')
binop_arith_instrs = {'+': Add, '-': Sub, '*': Mul, '/': Div}

Push = OperationFrame('push')
Pop = OperationFrame('pop')

        这部分主要研究了汇编语言的基本操作指令及其封装实现,为后面具体生成对应的代码提供方便。下一部分,我们开始分析汇编语言另一块非常重要的内容:栈帧结构

实现简易的C语言编译器(part 0)
实现简易的C语言编译器(part 1)
实现简易的C语言编译器(part 2)
实现简易的C语言编译器(part 3)
实现简易的C语言编译器(part 4)
实现简易的C语言编译器(part 5)
实现简易的C语言编译器(part 6)
实现简易的C语言编译器(part 7)
实现简易的C语言编译器(part 8)
实现简易的C语言编译器(part 9)
实现简易的C语言编译器(part 11)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,724评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,104评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,142评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,086评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,076评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,914评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,220评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,871评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,318评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,834评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,951评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,574评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,162评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,162评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,383评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,349评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,652评论 2 343

推荐阅读更多精彩内容