======Machine Language====== //Week 4 notes// ---- ====Overview==== * //Univeraslity//: 相同的**硬件**运行不同的**软件** //Turing// + //von Neumann// * 计算机通过机器语言(二进制指令)来实现软件层面上的不同功能 * 机器语言提供的的三个信息: * Operations:指令需要计算机做什么 * Counter:指令以及下一步指令是什么 * Addressing:指令需要操作哪些硬件 ===Compilcation=== * 计算机只能识别机器语言 * 人类需要通过编译器将人类友好的语言转化为机器语言 ==Mnemonics== 为了更方便的讨论机器语言,人们通常将指定的代码段与相应的含义(//Mnemonic Form//)对应起来。下面的例子中,''ADD'' 就是 ''0100010'' 对应的 //Mnemonic Form//。可以观察到的是,这种替代不仅可以代表 operation,也可以代表地址。 \\ \\ {{ :cs:comp_n_arch:courses:fnti_i:mnemonics.svg?300 |}} 需要注意的是: * 类似 ''ADD'' 这样的形式不是真正存在的 * 人类通过汇编语言(//assembly language//)来讲上述形式转化为机器语言 ==Symbols== Symbols 主要应用于简化内存寻址的过程。比如维护一个 ''index'',将指定的内存区域分为不同的片区,当需要对指定区域进行操作时,我们可以通过对应的 ''index'' 对其进行快速访问。这种将 symbol 与实际内存地址建立映射关系的方式,会大大增强程序的易读性和可维护性。该映射关系也是由 assmbler 来翻译的。 ====Machine Languages: Elements==== * 机器语言是硬件与软件的 Interface,其决定了硬件可以进行什么样的操作 * 硬件与软件层面的操作通常接近,但不一定一一对应 * 机器语言的复杂程度取决于设计。通常,设计的操作越复杂,硬件层面的实现花费就越高。 ===Machine operations=== * 算术运算:add / sub * 逻辑运算:and / or * 流程控制(flow control) 不同的机器语言之间的实现可能并不相同,取决于其复杂度。 ===Addressing=== 内存寻址是一项非常昂贵的操作。通常来说,有两个问题: * 内存越大,需要提供的地址就越长 * 内存越大,访问指定内存的时间就越长 ==Memory Hierarchy== 解决方案是提供内存的级联:将内存按速度进行划分,不同速度的内存区域对应不同的任务。需要注意的是,速度越快,对应的内存就越小。 ==Registers== 寄存器是访问速度最快的内存,通常由 CPU 内置。寄存器分为几个类型: * Data Registers:比如 ''add r1 r2'',''r1'' 和 ''r2'' 就是该加法操作中,对应数据所在的寄存器名称。 * Address Registers:类似于指针,作为某一片我们需要使用的内存区域的入口 ==Addressing Modes== # Register mode R2 <- R2 + R1 Add R1, R2 # Direct mode Mem[200] <- Mem[200] + R1 Add R1, M[200] # Indirect Mem[A] <- Mem[A] + R1 Add R1, @A # Immediate R1 <- R1 + 73 Add 73, R1 ==Input / Output== 外设(键盘鼠标)通常通过寄存器链接,并通过一定的协议(驱动)来使其工作。 ===Flow Control=== * 通常按顺序执行 * 某些情况需要进行无条件跳转(unconditional jump),比如 loop * 通常使用寄存器来存储每次迭代的结果 * loop 的起始点通常会使用一个名字与其地址相关联,方便跳转(比如 loop 关联到 102,则使用 Jump loop 等同于 jump 102) * 某些情况下需要进行带条件跳转 (condtional jump) ====The HACK Computer==== * 16 bits computer * Data memory: RAM 由一系列的 16-bit 寄存器组成, ''RAM[0]'' * Instruction memory: ROM 由一系列的 16-bits 寄存器序列组成, ''ROM[0]'' * CPU: 执行 16 bits 的指令 * Buses:数据 / 寻址的通道 ===Software=== 整个 program 是一系列的 Hack 16bit 指令组成的序列。 ==机器语言== * 16-bit A 类指令 * 16-bit C 类指令 ===Control=== * 在 ROM 中会预写入指令序列 * 使用 ''reset'' 唤起该序列,之后整个程序开始运行 ===Registers=== * D Register:16 bit, 数据类寄存器 * A Register:16 bit,数据类 / 地址类寄存器 * M Register: 16 bit RAM 寄存器,代表着当前地址 ''A'' 对应的寄存器 ===Instruction=== ==A-instruction== A指令(寻址类)的作用:存储一个值到某个 A 类寄存器中。该值将作为 RAM 的 index,自动关联到 M 寄存器。比如 ''@21'',之后对 M 类寄存器进行访问,实际上就是访问的 ''RAM[21]''。 # 对 A 类寄存器赋值 # 语法:@value # value 值:non-negative decimal constant / symbol referring to such a constant # side effects: RAM[21] becomes the selected RAM register(M 寄存器) @21 # usage @100 # A=100 M=-1 # RAM[100] = -1 ==C-instruction: overview== C指令(计算类)由下面的结构组成: # dest & jump are optional dest:comp ; jump 其中: * //Dest// 代表的是计算的结果所存放的寄存器,可以是 A / D / M 类寄存器 * //Comp// 代表的是计算表达式 * //Jump// 代表的是带条件的跳转,当 Comp 的结果匹配某个条件时,程序会跳转到 **ROM** 中对应的指令位置 整个流程: - 在 COMP 中计算 - 将计算的结果存到 dest 中 - 如果COMP的结果 ''=='' 0,那么执行 jump。跳转的位置与之前指定的 A 值相关 ==C-instruction: dest== 该部分指定计算结果应该存储到哪个寄存器。比如: # set the D resgister to -1 D=-1 # set the RAM[300] resgister to D - 1 # when use A register, it must be assigned a value first @300 # A = 300 M = D - 1 # RAM[300] = D - 1 ==C-instruction: jump== jump 是 C 指令的一个可选操作。通常来说,当 jump 是 C 指令的一部分时,整个 C 指令实际上就是为了 jump 而准备的。比如下面的例子: # if (D-1 == 0), jump to execute the instruction sotred in ROM[56] @56 D-1;JEQ 也就是说,这个 ''A = 56'' 是一个目的性很强的操作,它的主要功能就是告诉计算机当条件成立时,我们应该执行 ''ROM[56]'' 对应的指令。换句话说,jump 的意义就是我们希望在某种条件成立的情况下,执行特定的指令(如果没有 jump,ROM 里的指令会按次序进行)。 \\ \\ 根据条件的不同,HACK computer 中提供了多种不同条件的 jump: ^Type^explanation^ |//null//|不跳转(默认情况)| |//JGT//|如果 comp > 0,跳转| |//JEQ//|如果 comp == 0,跳转| |//JGE//|如果 comp >= 0,跳转| |//JLT//|如果 comp < 0,跳转| |//JNE//|如果 comp ≠ 0,跳转| |//JLE//|如果 comp <= 0,跳转| |//JMP//|无条件跳转(always jump| ====Language Specificaion==== 两种写指令的方式: * Symbolic:人类友好,需要 assmbler 来进行翻译 * Binary:二进制机器码 ===Comparsion in A-ins=== 两种指令的对比如下: # set the A register to value # symbolic @ + value # Binary 0 + value(in binary) @21 0 000000000010101 ===Comparsion in C-ins=== C-insturction 的主要难点在于如何用二进制表示 C-instruction: * 首先,Hack 是 16-bits 的计算机,因此指令的长度是 16 bits * 这 16 bits 被区分为了不同区域: * [0]:这一部分是 op code。C 指令对应的 op code 是 ''1'' * [1..2]:这一部分没有用到,约定的内容是两个 ''1'' * [3..9]: 这一部分代表的 COMP 部分的内容 * [10..12]: 这一部分代表的是 dest 部分的内容 * [13..15]: 这一部分代表的是 jump 部分的内容 ==Comp 部分的对比== {{ :cs:comp_n_arch:courses:fnti_i:comp_symbolic_binary.jpg?400 |}} ==dest 部分的对比== {{ :cs:comp_n_arch:courses:fnti_i:dest_symbolic_binary.jpg?400 |}} ==jump 部分的对比== {{ :cs:comp_n_arch:courses:fnti_i:jump_symbolic_binary_1.jpg?400 |}} ====In/Output==== * 键盘,鼠标,显示器:这些独立于计算机外部的设备通常被称为 peripheral I/O devices * 主要作用是于用户互动 ===控制 I/O 设备=== * High-level 方法:使用软件 / 语言的 lib * Low-level 方法:使用 Bits ===Output=== * RAM 中会单独提供一部分区域用于显示器的内容的存储 * 显示器通过对区域的内容进行持续刷新来更新输出的内容 * 输出的内容可以通过 coding 的方式来操控 ==Screen memory map== * 256 * 512, black / white * 通过一个 8k 的 16 bits 的 memory map 来作为其底层(256 * 512 == 8192 * 16),每个 bit 代表一个像素,1 / 0 代表 black / white ==map 的映射== * 以 32 个 字为单位作为一**行**(32 * 16 = 512) * 行的范围为 [0, 255] * 寻找坐标为(row, col) 的像素点: * 寻找其所在行(也就是对应的哪一个由32个寄存器组成的片区):32 * row * 寻找其所在列:col / 16 (整除,也就是找出片区内对应的寄存器) * 最后结果 i = 32*row + col / 16 * 再找到对应的像素点:col % 16 * 最后将内容更新到 RAM 里,在下一个更新期就可以看到更新的内容了 需要注意的是,上述的映射是以 screen 所在的内存区域为基准的。当 screen 作为整个内存的一部分时,需要将之前的量加上,比如: word = Screen[32*row + col/16] #without base addres word = RAM[16384 + 32*row + col/16] #with base address 当得到具体寄存器所在地址后,如果我们要改变该寄存器中任意一个像素点,可以使用 ''col % 16'' 来找到该像素点并赋值 ===Input=== * 键盘使用 keyboard memory map 来实现(单 16bits 寄存器) * 每个键盘按键都对应一定的值(类似 ASCII 值) * 当按下键盘的时候,按键对应的值(比如 ''k'' 对应的 75)就会以二进制的形式写入该寄存器 * Hack 计算器中,寄存器位置:''RAM[24576]''。如果其值为 ''0'',代表没有按键按下。 ====Hack Proramming==== 可以使用课程提供的 assambler 或者 cpu emulator 来进行 symbolic 语言到机器语言的转化。 Hack 编程语言需要的功能有: * Working with registers and memeory * Branching * Variables * Iteration * Pointers * Input / Output ===Registers & Memory=== 下面是一些基础的例子: # D = 10 @10 D = A # D++ D = D + 1 # D = RAM[17] @17 D = M # RAM[17] = 0 # 注意这里的 0 是表达式!而不是常量,HACK 里常量是不能直接写入 M 的 @17 M = 0 # RAM[17] = 10 # 注意,Hack 的指令集并不能支持将常量写入 M 的操作 # 所有的操作都必须放到寄存器里去过一遍 @10 # 将常量 10 写入到 A D = A # 之后 A 需要复用(写入 17,关联 M),因此这里将当前 A 代表的常量写入到 D @17 # 使用 A 取地址,准备关联 M M = D # 关联 M 到 RAM[17],并写入 D 中的常量 10 # RAM[5] = RAM[3] @3 D = M @5 M = D ==实例:两数相加== 指令在转换过程中会忽略所有的 white space。 # adds up two numbers # RAM[2] = RAM[0] + RAM[1] @0 D = M # D = RAM[0] @1 D = D + M # D = D + RAM[1] @2 M = D # RAM[2] = D 需要特别注意的是,**计算机并不会自动终结程序**。一种终止程序的解决方案是,在指令序列的末尾添加一个无限循环,让整个程序不会再继续执行下去: @6 0;JMP ==Built-in symbols== * R0 to R15:对应的数据为 0 到 15 这些 symbols 主要作为虚拟寄存器(//Virutal register//),来提高可读性。考虑下面的程序: @15 D=A @5 M=D 这段程序中,A 既充当了数据寄存器,又充当了地址寄存器。如果单看带 ''@'' 这一段,我们根本不知道 A 到底使用的哪个寄存器。R0-R15 代表着我们指定了将要使用的寄存器是 0-15 号寄存器,而不是别的: @R5 # case sensitive! no r5 M=D * SCREEN 16384 * KBD 24576 ===Branching=== 机器语言(汇编语言)中,只存在着一种 branching 的方式://goto//: # sinnum.asm # Computes: if R0>0, R1 = 1, else R1 = 0 @R0 D = M # ROM[8] 会针对另外一个条件做处理 # 如果 D > 0, 则跳转到 ROM[8] @8 D;JGT # 否则做另外一个分支 # 两个分支都会使用 R1 寄存器 @R1 M = 0 # 无限循环,结束程序(在分支 R0 <= 0 上) # 入口是 ROM[6],但会跳转到 ROM[10],无限循环最终会稳定在 ROM[10] @10 0;JMP # ROM[8], D > 0 分支 @R1 D = M # 做 D > 0 分支的结束 @10 0;JMP ==使用 label 提高可读性== 我们可以使用一些 label 代替具体的位置,来提高可读性: # jump to POSITIVE branch @POSITIVE D; JGT (POSITIVE) @R1 M = 1 # infinite loop # Jump to the @end (END) @END 0;JMP 括号中的内容可以被视作注释,不会被转写;但引用(''@END'')这个将在转写中被替代为对应的 ROM 入口。 ===Variables=== Hack 中使用 16bits 的寄存器作为 variable 的唯一标识方式。在Hack 语言中,我们可以通过 ''@varName'' 的方式来定义一个变量。这个变量存储于 RAM 中,其起始位置为 ''RAM[16]''(0-15 被 R0-R15 占了)。下面是一个具体的例子: # exhange two values # temp = R1, R1 = R0, R0 = temp @R1 D = M @temp #此时 M 对应的是变量所关联的寄存器 M = D @R2 D = M @R1 M = D # R1 = R0 @temp D = M # 再次关联 temp 的值到 M,并存储到 D 中 @R0 # 最后将该值存放到 R0 中,交换完毕 M = D (END) @END 0;JMP 需要注意的是,变量被识别的前提是在程序中无法找到与其对应的 label 注释。这个是与 branch 的根本不同: * Branch label 是 ''@name + (name)'' 的形式,分配位置与具体 label 所在位置有关 * 变量是 ''@name'' 的形式,分配位置从 ''RAM[16]'' 开始 ===Iteration=== 在 Hack 中写循环一般要确定两个入点: * STOP:入口放置于整个程序的末尾,跳转点放置于循环条件的后方 * LOOP:入口放置于循环体的起始点,跳转点放置于循环体的末尾 下面是一个循环累加的例子: # compute RAM[1] + RAM[2] ... + RAM[n] # 初始化 @R0 D=M @n M=D # 将 R0 的值用于 n 的初始化 @i M = 1 # i = 1 @sum M = 0 # sum = 0 (LOOP) @i D = M # 获取当前 i 值 @n D = D - M # 获取 n 值,计算 i-n 的结果 @STOP D;JGT # 如果 i-n > 0,则循环超过上限,停止 @sum D = M @i D = D+M # 否则,sum += i @sum # 覆写 sum M = D @i M = M + 1 # ++i @LOOP # 再次循环 0;JMP (STOP) @sum D = M @R1 # 将 sum 值存储到 R1 中 M = D (END) @END 0;JMP * 推荐先写伪代码,再翻译为 HACK program * 测试推荐使用 track table:跟踪每个迭代中每个值的变化,看是否符合预期 ===Pointers=== array 的形式在机器语言中是不存在的。取而代之的是,array 是以起始地址 + 偏移量的形式来进行访问的。我们将这种通过地址进行访问的形式称为指针(//Pointer//)。 实现上,我们会通过更新 A 来得到其对应的 M,再对 M进行更新操作。下面是一个例子: // 需要模拟的代码 n = 10; for (i = 0; i < n; ++i) { arr[i] = -1; } # 初始化 # 假设 array arr 的起始地址为 100 # 初始化 arr @100 D = A @arr M = D # 将 arr 的初始值设为 100 # 初始化 n @10 D = A @n M = D # 将 n 的初始值设为 10 # 初始化 i @i M = 0 # 将 i 的初始值设为 0 # 开始进入循环 (LOOP) # 如果 i == n,循环结束 @i D = M @n D = D - M # D 是判断条件,存储 i - n 的结果 @END D;JEQ # i == n 则跳转至 END # 如果不相等,则循环继续 # 获取 arr 的起始地址 @arr D=M @i A = D+M # 计算偏移量,也就是指针的地址 arr + i # 更新对应的地址 # 注意此时的 M 已经变化了,不再是 i 对应的寄存器,而是 A 对应的寄存器 # A 更新后访问 M 的 side effect: M 会与 RAM[A] 关联 M = -1 # RAM[arr+i] = -1 @i M = M + 1 # ++i @LOOP # 循环结束 0;JMP (END) @END 0;JMP ===Input & Output=== 这部分主要实现的是: * Screen * Keyboard {{ :cs:comp_n_arch:courses:fnti_i:hack_memory_distribution.svg?150 |}} ==实例:drawing an rectangle== * 题目的要求是:画一个自定义 row(''n'') 的,宽度(列)为 16 bits 的矩形。 首先从高级语言考虑一下这个代码的循环: for (i=0; i < n; ++i) { // print first black pixels at he beginning of row i } 接下来以更机器语言的方式思考该伪代码: # init addr = SCREEN # 写入的首地址是 SCREEN 对应的内存起始点 n = RAM[0] # 从 R0 中获取 n 的初始值 i = 0 # Looping LOOP: if i > n goto END # 循环结束条件是 i > n # draw pixels RAM[addr] = -1 # -1的二进制表示是 1111111111111111 # update status addr += 32 # 换行,由于屏幕宽度是 512,每一行需要 32 个寄存器才可以完全表示,因此换行是直接去第 33 个寄存器 i += 1 # 更新循环条件 goto LOOP END: goto END 实现如下: @SCREEN D = A @addr # 使用 SCREEN 地址初始化 addr, 16384 号寄存器 M = D @R0 D = M @n M = D # 使用 R0 内容初始化 n @i M = 0 # 初始化 i 为 0 (LOOP) @i D = M @n D = D - M @END D;JGT # 如果 i > n, 结束循环 @addr A = M M = -1 # RAM[addr] = 1111 1111 1111 1111 # 更新循环状态 @i M = M + 1 @32 D = A # 注意,所有的常量都需要用 A 类寄存器获取 @addr M = M + D @LOOP 0;JMP (END) @END 0;JMP ==keyboard== - 读取 RAM[24576] 内的内容 - 如果内容为 ''0'' ,那么没有键被按下 - 否则,搜寻对应值的 key 值