使用 Python 構建一個 CPU 模擬器

類別: IT
標籤: python

今天早上早些時候,在我的Planet Python源中,我讀到了一篇有趣的文章"開發CARDIAC:紙板計算機(Developing upwards: CARDIAC: The Cardboard Computer)",它是關於名為Cardiac的紙板計算機的.我的一些追隨者和讀者應該知道,我有一個名為簡單CPU(simple-cpu)的專案,過去的數月我一直工作於此,並且已經發布了原始碼.我真的應該給這個專案提供一個合適的許可證,這樣,其他人可能更感興趣,並在他們自己的專案中使用.不管怎樣,但願在這釋出之後,我可以完成這件事.

在讀完了這篇文章以及它連結的頁面後,我受到了一些啟發,決定為它編寫我自己的模擬器,因為我有編寫位元組碼引擎的經驗.我計劃著跟隨這篇文章繼續往前,先寫一篇關於彙編器的文章,接下來是關於編譯器的文章.這樣,通過這些文章,你基本上可以學到,如何用Python為Cardiac建立編譯工具集. 在簡單CPU(simple-cpu)專案中,我已經編寫了一個完整的可工作的彙編器.在內建的遊戲中,已經有了可工作的編譯器的最初步驟.我也選擇Cardiac作為一個驗證機器是因為它絕對的簡單.不需要複雜的記憶,每個操作碼只接受單一的引數,所以它是絕好的學習工具.此外,所有的資料引數都是相同的,不需要檢測程式是需要一個暫存器,字串或者還是記憶體地址.實際上,只有一個暫存器,累加器.因此,讓我們開始吧!我們將基於類來建立,這樣包含範圍.如果你想嘗試的話,你可以簡單通過子類來增加新的操作碼.首先,我們將集中於初始化例程.這個CPU非常簡單,所以我們只需要初始化下面的內容: CPU暫存器, 操作碼, 記憶體空間, 讀卡器/輸入, 和 列印/tty/輸出.

class Cardiac(object):
    """    This class is the cardiac "CPU".    """
    def __init__(self):
        self.init_cpu()
        self.reset()
        self.init_mem()
        self.init_reader()
        self.init_output()
    def reset(self):
        """        This method resets the CPU's registers to their defaults.        """
        self.pc = 0 #: Program Counter
        self.ir = 0 #: Instruction Register
        self.acc = 0 #: Accumulator
        self.running = False #: Are we running?
    def init_cpu(self):
        """        This fancy method will automatically build a list of our opcodes into a hash.        This enables us to build a typical case/select system in Python and also keeps        things more DRY.  We could have also used the getattr during the process()        method before, and wrapped it around a try/except block, but that looks        a bit messy.  This keeps things clean and simple with a nice one-to-one        call-map.         """
        self.__opcodes = {}
        classes = [self.__class__] #: This holds all the classes and base classes.
        while classes:
            cls = classes.pop() # Pop the classes stack and being
            if cls.__bases__: # Does this class have any base classes?
                classes = classes + list(cls.__bases__)
            for name in dir(cls): # Lets iterate through the names.
                if name[:7] == 'opcode_': # We only want opcodes here.
                    try:
                        opcode = int(name[7:])
                    except ValueError:
                        raise NameError('Opcodes must be numeric, invalid opcode: %s' % name[7:])
                    self.__opcodes.update({opcode:getattr(self, 'opcode_%s' % opcode)})
    def init_mem(self):
        """        This method resets the Cardiac's memory space to all blank strings, as per Cardiac specs.        """
        self.mem = ['' for i in range(0,100)]
        self.mem[0] = '001' #: The Cardiac bootstrap operation.
    def init_reader(self):
        """        This method initializes the input reader.        """
        self.reader = [] #: This variable can be accessed after initializing the class to provide input data.
    def init_output(self):
        """        This method initializes the output deck/paper/printer/teletype/etc...        """
        self.output = []

但願我寫的註釋能讓你們看明白程式碼的各部分功能.  也許你已經發現這段程式碼處理指令集的方法(method)跟 simple-cpu 專案有所不同. 由於它能讓開發者根據自己的需求輕鬆的擴充套件類庫, 我打算在後續的專案中繼續使用這種處理方式. 隨著我對各部分功能原理的深入理解, 專案也在不斷的發展變化. 其實吧,  做這樣一個專案真的能讓人學到不少東西.  對於精通計算機的人來說 ,  CPU 的工作原理啦, 指令集是怎麼處理的啦, 都不是問題啦 .  關鍵是, 能夠按照自己的想法去實現這樣一個 CPU 模擬器, 真的很好玩. 根據自己想象中的樣子, 親手打造出這樣一臺模擬器, 然後看著它屁顛屁顛的執行著, 那叫一個有成就感.

接下來, 我們講下工具函式(utility functions), 這些函式在很多地方都會用到, 而且允許在子類(subclasses)中重寫:

    def read_deck(self, fname):
        """        將指令讀到 reader 中.        """
        self.reader = [s.rstrip('\n') for s in open(fname, 'r').readlines()]
        self.reader.reverse()
    def fetch(self):
        """        根據指令指標(program pointer) 從記憶體中讀出指令, 然後將指令指標加1.        """
        self.ir = int(self.mem[self.pc])
        self.pc +=1
    def get_memint(self, data):
        """        由於我們是以字串形式(*string* based)儲存記憶體資料的, 要模擬 Cardiac, 就要將字串轉化成整數.  如果是其他儲存形式的記憶體, 如 mmap, 可以根據需要重寫本函式.        """
        return int(self.mem[data])
    def pad(self, data, length=3):
        """        本函式的功能是像 Cardiac 那樣, 在數字的前面補0.        """
        orig = int(data)
        padding = '0'*length
        data = '%s%s' % (padding, abs(data))
        if orig < 0:
            return '-'+data[-length:]
        return data[-length:]

本文後面我會另外給大家一段能結合 Mixin classes 使用的程式碼, 靈活性(pluggable)更強些.  最後就剩下這個處理指令集的方法了:

    def process(self):
        """        本函式只處理一條指令.  預設情況下, 從迴圈程式碼(running loop)中呼叫, 你也可以自己寫程式碼, 以單步除錯的方式呼叫它, 或者使用 time.sleep() 降低執行的速度.  如果想用 TK/GTK/Qt/curses 做的前端介面(frontend), 在另外一個執行緒中操作, 也可以呼叫本函式.        """
        self.fetch()
        opcode, data = int(math.floor(self.ir / 100)), self.ir % 100
        self.__opcodes[opcode](data)
    def opcode_0(self, data):
        """ 輸入指令 """
        self.mem[data] = self.reader.pop()
    def opcode_1(self, data):
        """ 清除指令 """
        self.acc = self.get_memint(data)
    def opcode_2(self, data):
        """ 加法指令 """
        self.acc += self.get_memint(data)
    def opcode_3(self, data):
        """ 測試累加器內容指令 """
        if self.acc < 0:
            self.pc = data
    def opcode_4(self, data):
        """ 位移指令 """
        x,y = int(math.floor(data / 10)), int(data % 10)
        for i in range(0,x):
            self.acc = (self.acc * 10) % 10000
        for i in range(0,y):
            self.acc = int(math.floor(self.acc / 10))
    def opcode_5(self, data):
        """ 輸出指令 """
        self.output.append(self.mem[data])
    def opcode_6(self, data):
        """ 儲存指令 """
        self.mem[data] = self.pad(self.acc)
    def opcode_7(self, data):
        """ 減法指令 """
        self.acc -= self.get_memint(data)
    def opcode_8(self, data):
        """ 無條件跳轉指令 """
        self.pc = data
    def opcode_9(self, data):
        """ 終止, 復位指令 """
        self.reset()
    def run(self, pc=None):
        """ 這段程式碼一直執行到遇到 終止/復位 指令為止. """
        if pc:
            self.pc = pc
        self.running = True
        while self.running:
            self.process()
        print "Output:\n%s" % '\n'.join(self.output)
        self.init_output()if __name__ == '__main__':
    c = Cardiac()
    c.read_deck('deck1.txt')
    try:
        c.run()
    except:
        print "IR: %s\nPC: %s\nOutput: %s\n" % (c.ir, c.pc, '\n'.join(c.output))
        raise

這段是上面提到的, 能在 Mixin 中使用的程式碼, 我重構過後, 程式碼如下 :

class Memory(object):
    """    本類實現模擬器的虛擬記憶體空間的各種功能    """
    def init_mem(self):
        """        用空白字串清除 Cardiac 系統記憶體中的所有資料        """
        self.mem = ['' for i in range(0,100)]
        self.mem[0] = '001' #: 啟動 Cardiac 系統.
    def get_memint(self, data):
        """        由於我們是以字串形式(*string* based)儲存記憶體資料的, 要模擬 Cardiac, 就要將字串轉化成整數.  如果是其他儲存形式的記憶體, 如 mmap, 可以根據需要重寫本函式.        """
        return int(self.mem[data])
    def pad(self, data, length=3):
        """        在數字前面補0        """
        orig = int(data)
        padding = '0'*length
        data = '%s%s' % (padding, abs(data))
        if orig < 0:
            return '-'+data[-length:]
        return data[-length:]
class IO(object):
    """    本類實現模擬器的 I/O 功能.    To enable alternate methods of input and output, swap this.    """
    def init_reader(self):
        """        初始化 reader.        """
        self.reader = [] #: 此變數在類初始化後, 可以用來讀取輸入的資料.
    def init_output(self):
        """        初始化諸如: deck/paper/printer/teletype/ 之類的輸出功能...        """
        self.output = []
    def read_deck(self, fname):
        """        將指令讀到 reader 中.        """
        self.reader = [s.rstrip('\n') for s in open(fname, 'r').readlines()]
        self.reader.reverse()
    def format_output(self):
        """        格式化虛擬 I/O 裝置的輸出(output)        """
        return '\n'.join(self.output)
    def get_input(self):
        """        獲取 IO 的輸入(input), 也就是說用 reader 讀取資料, 代替原來的 raw_input() .        """
        try:
            return self.reader.pop()
        except IndexError:
            # 如果 reader 遇到檔案結束標誌(EOF) 就用 raw_input() 代替 reader.
            return raw_input('INP: ')[:3]
    def stdout(self, data):
        self.output.append(data)
class CPU(object):
    """   本類模擬 cardiac CPU.    """
    def __init__(self):
        self.init_cpu()
        self.reset()
        try:
            self.init_mem()
        except AttributeError:
            raise NotImplementedError('You need to Mixin a memory-enabled class.')
        try:
            self.init_reader()
            self.init_output()
        except AttributeError:
            raise NotImplementedError('You need to Mixin a IO-enabled class.')
    def reset(self):
        """        用預設值重置 CPU 的暫存器        """
        self.pc = 0 #: 指令指標
        self.ir = 0 #: 指令暫存器
        self.acc = 0 #: 累加器
        self.running = False #: 模擬器的執行狀態?
    def init_cpu(self):
        """        本函式自動在雜湊表中建立指令集. 這樣我們就可以使用 case/select 方式呼叫指令, 同時保持程式碼簡潔.  當然, 在 process() 中使用 getattr 然後用 try/except 捕捉異常也是可以的, 但是程式碼看起來就沒那麼簡潔了.        """
        self.__opcodes = {}
        classes = [self.__class__] #: 獲取全部類, 包含基類.
        while classes:
            cls = classes.pop() # 把堆疊中的類彈出來
            if cls.__bases__: # 判斷有沒有基類
                classes = classes + list(cls.__bases__)
            for name in dir(cls): # 遍歷名稱.
                if name[:7] == 'opcode_': # 只需要把指令讀出來即可                    try:
                        opcode = int(name[7:])
                    except ValueError:
                        raise NameError('Opcodes must be numeric, invalid opcode: %s' % name[7:])
                    self.__opcodes.update({opcode:getattr(self, 'opcode_%s' % opcode)})
    def fetch(self):
        """        根據指令指標(program pointer) 從記憶體中讀取指令, 然後指令指標加 1.        """
        self.ir = self.get_memint(self.pc)
        self.pc +=1
    def process(self):
        """        處理當前指令, 只處理一條.  預設情況下是在迴圈程式碼中呼叫(running loop), 也可以自己寫程式碼, 以單步除錯方式呼叫, 或者利用 time.sleep() 降低執行速度.  在 TK/GTK/Qt/curses 做的介面的執行緒中呼叫本函式也是可以的.        """
        self.fetch()
        opcode, data = int(math.floor(self.ir / 100)), self.ir % 100
        self.__opcodes[opcode](data)
    def opcode_0(self, data):
        """ 輸入指令 """
        self.mem[data] = self.get_input()
    def opcode_1(self, data):
        """ 清除累加器指令 """
        self.acc = self.get_memint(data)
    def opcode_2(self, data):
        """ 加法指令 """
        self.acc += self.get_memint(data)
    def opcode_3(self, data):
        """ 測試累加器內容指令 """
        if self.acc < 0:
            self.pc = data
    def opcode_4(self, data):
        """ 位移指令 """
        x,y = int(math.floor(data / 10)), int(data % 10)
        for i in range(0,x):
            self.acc = (self.acc * 10) % 10000
        for i in range(0,y):
            self.acc = int(math.floor(self.acc / 10))
    def opcode_5(self, data):
        """ 輸出指令 """
        self.stdout(self.mem[data])
    def opcode_6(self, data):
        """ 儲存指令 """
        self.mem[data] = self.pad(self.acc)
    def opcode_7(self, data):
        """ 減法指令 """
        self.acc -= self.get_memint(data)
    def opcode_8(self, data):
        """ 無條件跳轉指令 """
        self.pc = data
    def opcode_9(self, data):
        """ 停止/復位指令"""
        self.reset()
    def run(self, pc=None):
        """ 這段程式碼會一直執行, 直到遇到 halt/reset 指令才停止. """
        if pc:
            self.pc = pc
        self.running = True
        while self.running:
            self.process()
        print "Output:\n%s" % self.format_output()
        self.init_output()
class Cardiac(CPU, Memory, IO):
    passif __name__ == '__main__':
    c = Cardiac()
    c.read_deck('deck1.txt')
    try:
        c.run()
    except:
        print "IR: %s\nPC: %s\nOutput: %s\n" % (c.ir, c.pc, c.format_output())
        raise

大家可以從 Developing Upwards: CARDIAC: The Cardboard Computer 中找到本文使用的 deck1.txt .

希望本文能啟發大家, 怎麼去設計基於類的模組, 插拔性強(pluggable)的 Paython 程式碼, 以及如何開發 CPU 模擬器.   至於本文 CPU 用到的彙編編譯器(assembler) , 會在下一篇文章中教大家.

這段是上面提到的, 能在 Mixin 中使用的程式碼, 我重構過後, 程式碼如下 :

class Memory(object):
    """    本類實現模擬器的虛擬記憶體空間的各種功能    """
    def init_mem(self):
        """        用空白字串清除 Cardiac 系統記憶體中的所有資料        """
        self.mem = ['' for i in range(0,100)]
        self.mem[0] = '001' #: 啟動 Cardiac 系統.
    def get_memint(self, data):
        """        由於我們是以字串形式(*string* based)儲存記憶體資料的, 要模擬 Cardiac, 就要將字串轉化成整數.  如果是其他儲存形式的記憶體, 如 mmap, 可以根據需要重寫本函式.        """
        return int(self.mem[data])
    def pad(self, data, length=3):
        """        在數字前面補0        """
        orig = int(data)
        padding = '0'*length
        data = '%s%s' % (padding, abs(data))
        if orig < 0:
            return '-'+data[-length:]
        return data[-length:]
class IO(object):
    """    本類實現模擬器的 I/O 功能.    To enable alternate methods of input and output, swap this.    """
    def init_reader(self):
        """        初始化 reader.        """
        self.reader = [] #: 此變數在類初始化後, 可以用來讀取輸入的資料.
    def init_output(self):
        """        初始化諸如: deck/paper/printer/teletype/ 之類的輸出功能...        """
        self.output = []
    def read_deck(self, fname):
        """        將指令讀到 reader 中.        """
        self.reader = [s.rstrip('\n') for s in open(fname, 'r').readlines()]
        self.reader.reverse()
    def format_output(self):
        """        格式化虛擬 I/O 裝置的輸出(output)        """
        return '\n'.join(self.output)
    def get_input(self):
        """        獲取 IO 的輸入(input), 也就是說用 reader 讀取資料, 代替原來的 raw_input() .        """
        try:
            return self.reader.pop()
        except IndexError:
            # 如果 reader 遇到檔案結束標誌(EOF) 就用 raw_input() 代替 reader.
            return raw_input('INP: ')[:3]
    def stdout(self, data):
        self.output.append(data)
class CPU(object):
    """   本類模擬 cardiac CPU.    """
    def __init__(self):
        self.init_cpu()
        self.reset()
        try:
            self.init_mem()
        except AttributeError:
            raise NotImplementedError('You need to Mixin a memory-enabled class.')
        try:
            self.init_reader()
            self.init_output()
        except AttributeError:
            raise NotImplementedError('You need to Mixin a IO-enabled class.')
    def reset(self):
        """        用預設值重置 CPU 的暫存器        """
        self.pc = 0 #: 指令指標
        self.ir = 0 #: 指令暫存器
        self.acc = 0 #: 累加器
        self.running = False #: 模擬器的執行狀態?
    def init_cpu(self):
        """        本函式自動在雜湊表中建立指令集. 這樣我們就可以使用 case/select 方式呼叫指令, 同時保持程式碼簡潔.  當然, 在 process() 中使用 getattr 然後用 try/except 捕捉異常也是可以的, 但是程式碼看起來就沒那麼簡潔了.        """
        self.__opcodes = {}
        classes = [self.__class__] #: 獲取全部類, 包含基類.
        while classes:
            cls = classes.pop() # 把堆疊中的類彈出來
            if cls.__bases__: # 判斷有沒有基類
                classes = classes + list(cls.__bases__)
            for name in dir(cls): # 遍歷名稱.
                if name[:7] == 'opcode_': # 只需要把指令讀出來即可                    try:
                        opcode = int(name[7:])
                    except ValueError:
                        raise NameError('Opcodes must be numeric, invalid opcode: %s' % name[7:])
                    self.__opcodes.update({opcode:getattr(self, 'opcode_%s' % opcode)})
    def fetch(self):
        """        根據指令指標(program pointer) 從記憶體中讀取指令, 然後指令指標加 1.        """
        self.ir = self.get_memint(self.pc)
        self.pc +=1
    def process(self):
        """        處理當前指令, 只處理一條.  預設情況下是在迴圈程式碼中呼叫(running loop), 也可以自己寫程式碼, 以單步除錯方式呼叫, 或者利用 time.sleep() 降低執行速度.  在 TK/GTK/Qt/curses 做的介面的執行緒中呼叫本函式也是可以的.        """
        self.fetch()
        opcode, data = int(math.floor(self.ir / 100)), self.ir % 100
        self.__opcodes[opcode](data)
    def opcode_0(self, data):
        """ 輸入指令 """
        self.mem[data] = self.get_input()
    def opcode_1(self, data):
        """ 清除累加器指令 """
        self.acc = self.get_memint(data)
    def opcode_2(self, data):
        """ 加法指令 """
        self.acc += self.get_memint(data)
    def opcode_3(self, data):
        """ 測試累加器內容指令 """
        if self.acc < 0:
            self.pc = data
    def opcode_4(self, data):
        """ 位移指令 """
        x,y = int(math.floor(data / 10)), int(data % 10)
        for i in range(0,x):
            self.acc = (self.acc * 10) % 10000
        for i in range(0,y):
            self.acc = int(math.floor(self.acc / 10))
    def opcode_5(self, data):
        """ 輸出指令 """
        self.stdout(self.mem[data])
    def opcode_6(self, data):
        """ 儲存指令 """
        self.mem[data] = self.pad(self.acc)
    def opcode_7(self, data):
        """ 減法指令 """
        self.acc -= self.get_memint(data)
    def opcode_8(self, data):
        """ 無條件跳轉指令 """
        self.pc = data
    def opcode_9(self, data):
        """ 停止/復位指令"""
        self.reset()
    def run(self, pc=None):
        """ 這段程式碼會一直執行, 直到遇到 halt/reset 指令才停止. """
        if pc:
            self.pc = pc
        self.running = True
        while self.running:
            self.process()
        print "Output:\n%s" % self.format_output()
        self.init_output()
class Cardiac(CPU, Memory, IO):
    passif __name__ == '__main__':
    c = Cardiac()
    c.read_deck('deck1.txt')
    try:
        c.run()
    except:
        print "IR: %s\nPC: %s\nOutput: %s\n" % (c.ir, c.pc, c.format_output())
        raise

大家可以從 Developing Upwards: CARDIAC: The Cardboard Computer 中找到本文使用的 deck1.txt 的程式碼, 我用的是 從 1 計數到 10 的那個例子 .

希望本文能啟發大家, 怎麼去設計基於類的模組, 插拔性強(pluggable)的 Paython 程式碼, 以及如何開發 CPU 模擬器.   至於本文 CPU 用到的彙編編譯器(assembler) , 會在下一篇文章中教大家.

(譯者注:下面是 deck1.txt 的程式碼, 更多例子請點這裡)

002
800
010
100
011
605
012
104
013
322
014
505
015
105
016
200
017
605
018
104
019
700
020
604
021
812
022
900
004
009
002
810

使用 Python 構建一個 CPU 模擬器原文請看這裡