20、深入理解虚拟机器:架构、执行与汇编编程

深入理解虚拟机器:架构、执行与汇编编程

在计算机编程领域,理解底层机制对于提升编程技能和解决复杂问题至关重要。本文将深入探讨虚拟机器的架构、指令执行方式、汇编程序的编写,以及数据存储等关键概念。

1. 虚拟机器架构概述

计算机不能直接执行 JavaScript 等高级语言,而是通过编译器将高级语言转换为特定处理器的指令集。为了更好地理解 JavaScript 的运行机制,我们将模拟一个简单的处理器和内存环境。

虚拟机器主要由以下三部分组成:
- 指令指针(IP) :保存下一条要执行指令的内存地址,自动初始化为地址 0,这是虚拟机器应用二进制接口(ABI)的一部分。
- 寄存器(R0 - R3) :指令可以直接访问的四个寄存器,虚拟机器中没有内存到内存的操作,所有操作都通过寄存器进行。
- 内存(256 字) :每个字可以存储一个值,程序和数据都存储在这块内存中,选择 256 的大小是为了让每个地址能存储在一个字节中。

虚拟机器的指令长度为 3 字节,操作码占 1 字节,每条指令可包含一到两个单字节操作数。操作数可以是寄存器标识符、常量或地址。以下是虚拟机器的操作码列表:
| 指令 | 代码 | 格式 | 操作 | 示例 | 等效操作 |
| — | — | — | — | — | — |
| hlt | 1 | – | 停止程序 | hlt | process.exit(0) |
| ldc | 2 | rc | 加载立即数 | ldc R0 123 | R0 := 123 |
| ldr | 3 | rr | 加载寄存器 | ldr R0 R1 | R0 := RAM[R1] |
| cpy | 4 | rr | 复制寄存器 | cpy R0 R1 | R0 := R1 |
| str | 5 | rr | 存储寄存器 | str R0 R1 | RAM[R1] := R0 |
| add | 6 | rr | 加法 | add R0 R1 | R0 := R0 + R1 |
| sub | 7 | rr | 减法 | sub R0 R1 | R0 := R0 - R1 |
| beq | 8 | ra | 相等则跳转 | beq R0 123 | if (R0 === 0) PC := 123 |
| bne | 9 | ra | 不相等则跳转 | bne R0 123 | if (R0 !== 0) PC := 123 |
| prr | 10 | r- | 打印寄存器 | prr R0 | console.log(R0) |
| prm | 11 | r- | 打印内存 | prm R0 | console.log(RAM[R0]) |

我们将虚拟机器的架构细节存储在一个文件中,方便其他组件共享:

const OPS = {
    hlt: { code: 1, fmt: '--' }, // Halt program
    ldc: { code: 2, fmt: 'rv ' }, // Load immediate
    ldr: { code: 3, fmt: 'rr ' }, // Load register
    cpy: { code: 4, fmt: 'rr ' }, // Copy register
    str: { code: 5, fmt: 'rr ' }, // Store register
    add: { code: 6, fmt: 'rr ' }, // Add
    sub: { code: 7, fmt: 'rr ' }, // Subtract
    beq: { code: 8, fmt: 'rv ' }, // Branch if equal
    bne: { code: 9, fmt: 'rv ' }, // Branch if not equal
    prr: { code: 10, fmt: 'r-' }, // Print register
    prm: { code: 11, fmt: 'r-' } // Print memory
};
const OP_MASK = 0xFF; // select a single byte
const OP_SHIFT = 8; // shift up by one byte
const OP_WIDTH = 6; // op width in characters when printing
const NUM_REG = 4; // number of registers
const RAM_LEN = 256; // number of words in RAM
export { OPS, OP_MASK, OP_SHIFT, OP_WIDTH, NUM_REG, RAM_LEN };
2. 指令执行过程

为了执行指令,我们将类拆分为几个部分。首先定义一个基础类 VirtualMachineBase ,包含指令指针、寄存器、内存和输出提示:

import assert from 'assert';
import { OP_MASK, OP_SHIFT, NUM_REG, RAM_LEN } from './architecture.js';
const COLUMNS = 4;
const DIGITS = 8;
class VirtualMachineBase {
    constructor() {
        this.ip = 0;
        this.reg = Array(NUM_REG);
        this.ram = Array(RAM_LEN);
        this.prompt = '>>';
    }
}
export default VirtualMachineBase;

加载程序时,将指令数组复制到内存中,并重置指令指针和寄存器:

initialize(program) {
    assert(program.length <= this.ram.length, 'Program is too long for memory');
    for (let i = 0; i < this.ram.length; i += 1) {
        if (i < program.length) {
            this.ram[i] = program[i];
        } else {
            this.ram[i] = 0;
        }
    }
    this.ip = 0;
    this.reg.fill(0);
}

处理下一条指令时,虚拟机器获取指令指针指向的内存值,并将指令指针移动到下一个地址。然后使用位运算提取操作码和操作数:

fetch() {
    assert((0 <= this.ip) && (this.ip < RAM_LEN), `Program counter ${this.ip} out of range 0..${RAM_LEN}`);
    let instruction = this.ram[this.ip];
    this.ip += 1;
    const op = instruction & OP_MASK;
    instruction >>= OP_SHIFT;
    const arg0 = instruction & OP_MASK;
    instruction >>= OP_SHIFT;
    const arg1 = instruction & OP_MASK;
    return [op, arg0, arg1];
}

接下来,扩展基础类,添加 run 方法来执行程序:

import assert from 'assert';
import { OPS } from './architecture.js';
import VirtualMachineBase from './vm-base.js';
class VirtualMachine extends VirtualMachineBase {
    run() {
        let running = true;
        while (running) {
            const [op, arg0, arg1] = this.fetch();
            switch (op) {
                case OPS.hlt.code:
                    running = false;
                    break;
                case OPS.ldc.code:
                    this.assertIsRegister(arg0, op);
                    this.reg[arg0] = arg1;
                    break;
                default:
                    assert(false, `Unknown op ${op}`);
                    break;
            }
        }
    }
    assertIsRegister(reg) {
        assert((0 <= reg) && (reg < this.reg.length), `Invalid register ${reg}`);
    }
    assertIsAddress(addr) {
        assert((0 <= addr) && (addr < this.ram.length), `Invalid register ${addr}`);
    }
}
export default VirtualMachine;

以下是几个指令的执行示例:
- 存储寄存器

case OPS.str.code:
    this.assertIsRegister(arg0, op);
    this.assertIsRegister(arg1, op);
    this.assertIsAddress(this.reg[arg1], op);
    this.ram[this.reg[arg1]] = this.reg[arg0];
    break;
  • 加法
case OPS.add.code:
    this.assertIsRegister(arg0, op);
    this.assertIsRegister(arg1, op);
    this.reg[arg0] += this.reg[arg1];
    break;
  • 相等跳转
case OPS.beq.code:
    this.assertIsRegister(arg0, op);
    this.assertIsAddress(arg1, op);
    if (this.reg[arg0] === 0) {
        this.ip = arg1;
    }
    break;
3. 汇编程序示例

手动计算数值操作码比较繁琐,使用汇编器可以更方便地编写程序。汇编语言中的每个命令都对应虚拟机器的一条指令。以下是一个打印 R1 中值并停止的汇编程序:

# Print initial contents of R1.
prr R1
hlt

其数值表示为:

00010a
000001

汇编语言的一个特点是可以使用地址标签。例如,以下程序打印 0 到 2 的数字:

# Count up to 3.
# - R0: loop index.
# - R1: loop limit.
ldc R0 0
ldc R1 3
loop:
prr R0
ldc R2 1
add R0 R2
cpy R2 R1
sub R2 R0
bne R2 @loop
hlt

其执行流程如下:

graph TD;
    A[R0 := 0] --> B[R1 := 3];
    B --> C[print R0];
    C --> D[R0 += 1];
    D --> E[R2 := R1 - R0];
    E --> F{R2 == 0};
    F -- false --> C;
    F -- true --> G[halt];

汇编器的实现主要包括以下步骤:
1. 清理代码行,去除空白和注释。
2. 查找标签地址。
3. 编译每条指令。
4. 将编译后的指令转换为文本格式。

assemble(lines) {
    lines = this.cleanLines(lines);
    const labels = this.findLabels(lines);
    const instructions = lines.filter(line => !this.isLabel(line));
    const compiled = instructions.map(instr => this.compile(instr, labels));
    const program = this.instructionsToText(compiled);
    return program;
}
cleanLines(lines) {
    return lines
      .map(line => line.trim())
      .filter(line => line.length > 0)
      .filter(line => !this.isComment(line));
}
isComment(line) {
    return line.startsWith('#');
}
findLabels(lines) {
    const result = {};
    let index = 0;
    lines.forEach(line => {
        if (this.isLabel(line)) {
            const label = line.slice(0, -1);
            assert(!(label in result), `Duplicate label ${label}`);
            result[label] = index;
        } else {
            index += 1;
        }
    });
    return result;
}
isLabel(line) {
    return line.endsWith(':');
}
compile(instruction, labels) {
    const [op, ...args] = instruction.split(/\s+/);
    assert(op in OPS, `Unknown operation "${op}"`);
    let result = 0;
    switch (OPS[op].fmt) {
        case '--':
            result = this.combine(OPS[op].code);
            break;
        case 'r-':
            result = this.combine(this.register(args[0]), OPS[op].code);
            break;
        case 'rr ':
            result = this.combine(this.register(args[1]), this.register(args[0]), OPS[op].code);
            break;
        case 'rv ':
            result = this.combine(this.value(args[1], labels), this.register(args[0]), OPS[op].code);
            break;
        default:
            assert(false, `Unknown instruction format ${OPS[op].fmt}`);
    }
    return result;
}
combine(...args) {
    assert(args.length > 0, 'Cannot combine no arguments');
    let result = 0;
    for (const a of args) {
        result <<= OP_SHIFT;
        result |= a;
    }
    return result;
}
instructionsToText(program) {
    return program.map(op => op.toString(16).padStart(OP_WIDTH, '0'));
}
register(token) {
    assert(token[0] === 'R', `Register "${token}" does not start with 'R'`);
    const r = parseInt(token.slice(1));
    assert((0 <= r) && (r < NUM_REG), `Illegal register ${token}`);
    return r;
}
value(token, labels) {
    if (token[0] !== '@') {
        return parseInt(token);
    }
    const labelName = token.slice(1);
    assert(labelName in labels, `Unknown label "${token}"`);
    return labels[labelName];
}
4. 数据存储与数组分配

为了更方便地编写程序,我们可以在汇编器中添加数组支持。通过 .data 标记数据段的开始,并使用 label: number 为数组分配存储空间。

修改后的汇编器主要步骤如下:
1. 拆分代码行为指令和数据分配。
2. 查找标签地址。
3. 处理数据分配,为每个分配创建标签。
4. 编译指令。
5. 将编译后的指令转换为文本格式。

assemble(lines) {
    lines = this.cleanLines(lines);
    const [toCompile, toAllocate] = this.splitAllocations(lines);
    const labels = this.findLabels(lines);
    const instructions = toCompile.filter(line => !this.isLabel(line));
    const baseOfData = instructions.length;
    this.addAllocations(baseOfData, labels, toAllocate);
    const compiled = instructions.map(instr => this.compile(instr, labels));
    const program = this.instructionsToText(compiled);
    return program;
}
splitAllocations(lines) {
    const split = lines.indexOf(DIVIDER);
    if (split === -1) {
        return [lines, []];
    } else {
        return [lines.slice(0, split), lines.slice(split + 1)];
    }
}
addAllocations(baseOfData, labels, toAllocate) {
    toAllocate.forEach(alloc => {
        const fields = alloc.split(':').map(a => a.trim());
        assert(fields.length === 2, `Invalid allocation directive "${alloc}"`);
        const [label, numWordsText] = fields;
        assert(!(label in labels), `Duplicate label "${label}" in data allocation`);
        const numWords = parseInt(numWordsText);
        assert((baseOfData + numWords) < RAM_LEN, `Allocation "${label}" requires too much memory`);
        labels[label] = baseOfData;
        baseOfData += numWords;
    });
}

以下是一个填充数组的示例程序:

# Count up to 3.
# - R0: loop index.
# - R1: loop limit.
# - R2: array index.
# - R3: temporary.
ldc R0 0
ldc R1 3
ldc R2 @array
loop:
str R0 R2
ldc R3 1
add R0 R3
add R2 R3
cpy R3 R1
sub R3 R0
bne R3 @loop
hlt
.data
array: 10

通过以上步骤,我们可以更深入地理解虚拟机器的工作原理,包括架构、指令执行、汇编编程和数据存储。这些知识对于理解计算机底层机制和编写高效程序非常有帮助。

5. 练习题与拓展应用

为了进一步巩固对虚拟机器和汇编语言的理解,下面给出一些练习题和拓展应用,帮助大家深入掌握相关知识。

5.1 交换寄存器值

编写一个汇编语言程序,交换 R1 和 R2 中的值,同时不影响其他寄存器的值。

# Swap values in R1 and R2
ldc R3 0  # Use R3 as a temporary register
cpy R3 R1
cpy R1 R2
cpy R2 R3
hlt
5.2 反转数组

编写一个汇编语言程序,反转数组中的元素。程序开始时,一个字存储数组的基地址,下一个字存储数组的长度 N,紧接着是 N 个元素。

# Reverse an array
# - R0: start index
# - R1: end index
# - R2: array base address
# - R3: temporary
ldc R2 @array  # Load array base address
ldc R0 0  # Start index
ldc R1 @array_length  # End index
sub R1 1  # Adjust end index
loop:
    ldr R3 R2+R0  # Load value at start index
    ldr R4 R2+R1  # Load value at end index
    str R4 R2+R0  # Store value at end index to start index
    str R3 R2+R1  # Store value at start index to end index
    add R0 1  # Increment start index
    sub R1 1  # Decrement end index
    bne R0 R1 @loop  # If start index != end index, continue loop
hlt
.data
array: 10  # Allocate space for array
array_length: 10  # Array length
5.3 增加自增和自减指令
  1. 添加 inc dec 指令,分别对寄存器的值加 1 和减 1。
// Update OPS object in architecture.js
const OPS = {
    // ... existing instructions
    inc: { code: 12, fmt: 'r-' }, // Increment register
    dec: { code: 13, fmt: 'r-' } // Decrement register
};
  1. 修改 VirtualMachine 类的 run 方法来处理新指令:
class VirtualMachine extends VirtualMachineBase {
    run() {
        let running = true;
        while (running) {
            const [op, arg0, arg1] = this.fetch();
            switch (op) {
                // ... existing cases
                case OPS.inc.code:
                    this.assertIsRegister(arg0, op);
                    this.reg[arg0] += 1;
                    break;
                case OPS.dec.code:
                    this.assertIsRegister(arg0, op);
                    this.reg[arg0] -= 1;
                    break;
                default:
                    assert(false, `Unknown op ${op}`);
                    break;
            }
        }
    }
    // ... existing methods
}
  1. 重写之前的示例程序,使用新指令。例如,之前的计数程序可以简化为:
# Count up to 3 using inc
# - R0: loop index
# - R1: loop limit
ldc R0 0
ldc R1 3
loop:
    prr R0
    inc R0
    cpy R2 R1
    sub R2 R0
    bne R2 @loop
hlt

使用新指令后,程序变得更短且更易读。

5.4 使用长地址
  1. 修改虚拟机器,使 ldr str 指令包含 16 位地址,同时将虚拟机器的内存增加到 64K 字。
// Update constants in architecture.js
const RAM_LEN = 65536; // 64K words
const OP_SHIFT = 16; // Shift up by two bytes
  1. 修改 fetch 方法以处理 16 位地址:
fetch() {
    assert((0 <= this.ip) && (this.ip < RAM_LEN), `Program counter ${this.ip} out of range 0..${RAM_LEN}`);
    let instruction = this.ram[this.ip];
    this.ip += 1;
    const op = instruction & OP_MASK;
    instruction >>= OP_SHIFT;
    const arg0 = instruction & 0xFFFF; // 16-bit address
    instruction >>= OP_SHIFT;
    const arg1 = instruction & 0xFFFF; // 16-bit address
    return [op, arg0, arg1];
}

这种修改使指令解释变得复杂,因为需要处理更大的地址范围和更多的位操作。

5.5 字符串操作

在 C 语言中,字符串以非零字节开头,以零字节结尾。
1. 编写一个程序,计算字符串的长度(不包括终止符),程序开始时 R1 存储字符串的基地址,结束时 R1 存储字符串的长度。

# Calculate string length
# - R1: string base address
# - R2: length counter
ldc R2 0  # Initialize length counter
loop:
    ldr R3 R1+R2  # Load character at current position
    beq R3 0 @end  # If character is null, end loop
    add R2 1  # Increment length counter
    bne R3 0 @loop  # Continue loop
end:
cpy R1 R2  # Store length in R1
hlt
  1. 编写一个程序,将字符串复制到另一个内存块中。程序开始时,R1 存储源字符串的基地址,R2 存储目标内存块的基地址。
# Copy a string
# - R1: source string base address
# - R2: destination base address
# - R3: temporary
loop:
    ldr R3 R1  # Load character from source
    str R3 R2  # Store character to destination
    beq R3 0 @end  # If character is null, end loop
    add R1 1  # Increment source address
    add R2 1  # Increment destination address
    bne R3 0 @loop  # Continue loop
end:
hlt
  1. 如果字符串缺少终止符,第一个程序会继续读取内存,直到遇到零字节,可能会导致越界访问。第二个程序会不断复制内存,直到遇到零字节,同样可能导致越界访问。
5.6 调用和返回
  1. 在虚拟机器中添加一个名为 SP 的寄存器,作为栈指针,自动初始化为内存的最后一个地址。
class VirtualMachineBase {
    constructor() {
        this.ip = 0;
        this.reg = Array(NUM_REG);
        this.ram = Array(RAM_LEN);
        this.prompt = '>>';
        this.SP = RAM_LEN - 1; // Initialize stack pointer
    }
}
  1. 添加 psh 指令,将寄存器的值复制到 SP 指向的地址,然后将 SP 减 1。
// Update OPS object in architecture.js
const OPS = {
    // ... existing instructions
    psh: { code: 14, fmt: 'r-' } // Push register to stack
};

修改 VirtualMachine 类的 run 方法:

class VirtualMachine extends VirtualMachineBase {
    run() {
        let running = true;
        while (running) {
            const [op, arg0, arg1] = this.fetch();
            switch (op) {
                // ... existing cases
                case OPS.psh.code:
                    this.assertIsRegister(arg0, op);
                    this.ram[this.SP] = this.reg[arg0];
                    this.SP -= 1;
                    break;
                // ...
            }
        }
    }
    // ...
}
  1. 添加 pop 指令,将 SP 加 1,然后将该地址的值复制到寄存器中。
// Update OPS object in architecture.js
const OPS = {
    // ... existing instructions
    pop: { code: 15, fmt: 'r-' } // Pop register from stack
};

修改 VirtualMachine 类的 run 方法:

class VirtualMachine extends VirtualMachineBase {
    run() {
        let running = true;
        while (running) {
            const [op, arg0, arg1] = this.fetch();
            switch (op) {
                // ... existing cases
                case OPS.pop.code:
                    this.assertIsRegister(arg0, op);
                    this.SP += 1;
                    this.reg[arg0] = this.ram[this.SP];
                    break;
                // ...
            }
        }
    }
    // ...
}
  1. 使用这些指令,编写一个子程序,对数组中的每个值计算 2x + 1
# Evaluate 2x + 1 for each value in an array
# - R0: array base address
# - R1: array length
# - R2: index
# - R3: temporary
ldc R0 @array  # Load array base address
ldc R1 @array_length  # Load array length
ldc R2 0  # Initialize index
loop:
    ldr R3 R0+R2  # Load value from array
    ldc R4 2
    mul R3 R4  # Multiply by 2
    add R3 1  # Add 1
    str R3 R0+R2  # Store result back to array
    add R2 1  # Increment index
    bne R2 R1 @loop  # If index != length, continue loop
hlt
.data
array: 10  # Allocate space for array
array_length: 10  # Array length
5.7 反汇编指令

编写一个反汇编器,将机器指令转换为汇编代码。由于地址标签不存储在机器指令中,反汇编器通常生成类似 @L001 @L002 的标签。

function disassemble(program) {
    let labels = {};
    let labelCount = 0;
    let result = [];
    for (let i = 0; i < program.length; i++) {
        let instruction = program[i];
        let op = instruction & OP_MASK;
        instruction >>= OP_SHIFT;
        let arg0 = instruction & OP_MASK;
        instruction >>= OP_SHIFT;
        let arg1 = instruction & OP_MASK;
        let opName = Object.keys(OPS).find(key => OPS[key].code === op);
        let line = '';
        if (opName === 'beq' || opName === 'bne') {
            if (!(arg1 in labels)) {
                labels[arg1] = `@L${String(labelCount).padStart(3, '0')}`;
                labelCount++;
            }
            line = `${opName} R${arg0} ${labels[arg1]}`;
        } else if (opName === 'ldc') {
            line = `${opName} R${arg0} ${arg1}`;
        } else if (opName === 'ldr' || opName === 'cpy' || opName === 'str' || opName === 'add' || opName === 'sub') {
            line = `${opName} R${arg0} R${arg1}`;
        } else if (opName === 'prr' || opName === 'prm') {
            line = `${opName} R${arg0}`;
        } else if (opName === 'hlt') {
            line = opName;
        }
        result.push(line);
    }
    return result;
}
5.8 链接多个文件
  1. 修改汇编器,处理 .include 指令。
function assemble(lines) {
    lines = this.cleanLines(lines);
    let allLines = [];
    for (let line of lines) {
        if (line.startsWith('.include')) {
            let filename = line.split(' ')[1];
            let includedLines = readFile(filename); // Assume readFile is a function to read a file
            allLines = allLines.concat(includedLines);
        } else {
            allLines.push(line);
        }
    }
    const [toCompile, toAllocate] = this.splitAllocations(allLines);
    const labels = this.findLabels(allLines);
    const instructions = toCompile.filter(line => !this.isLabel(line));
    const baseOfData = instructions.length;
    this.addAllocations(baseOfData, labels, toAllocate);
    const compiled = instructions.map(instr => this.compile(instr, labels));
    const program = this.instructionsToText(compiled);
    return program;
}
  1. 处理重复标签名时,可以在发现重复标签时抛出错误。为了防止无限包含,可以使用一个集合记录已经包含的文件,在包含文件前检查是否已经包含过。
let includedFiles = new Set();
function assemble(lines) {
    lines = this.cleanLines(lines);
    let allLines = [];
    for (let line of lines) {
        if (line.startsWith('.include')) {
            let filename = line.split(' ')[1];
            if (includedFiles.has(filename)) {
                throw new Error(`Infinite include detected: ${filename}`);
            }
            includedFiles.add(filename);
            let includedLines = readFile(filename); // Assume readFile is a function to read a file
            allLines = allLines.concat(includedLines);
        } else {
            allLines.push(line);
        }
    }
    // ... rest of the assemble function
}
5.9 提供系统调用

修改虚拟机器,使开发者可以添加“系统调用”。
1. 在启动时,虚拟机器加载 syscalls.js 文件中定义的函数数组。

import syscalls from './syscalls.js';
class VirtualMachine extends VirtualMachineBase {
    constructor() {
        super();
        this.syscalls = syscalls;
    }
    run() {
        let running = true;
        while (running) {
            const [op, arg0, arg1] = this.fetch();
            switch (op) {
                // ... existing cases
                case OPS.sys.code:
                    let func = this.syscalls[arg0];
                    let result = func(this.reg[0], this.reg[1], this.reg[2], this.reg[3]);
                    this.reg[0] = result;
                    break;
                // ...
            }
        }
    }
    // ...
}
  1. sys 指令接受一个单字节常量参数,查找对应的函数并调用,将 R0 - R3 的值作为参数,将结果存储在 R0 中。
5.10 单元测试
  1. 编写汇编器的单元测试。可以使用测试框架如 Mocha 和 Chai 进行测试。
import { expect } from 'chai';
import { assemble } from './assembler.js';
describe('Assembler', () => {
    it('should assemble a simple program correctly', () => {
        let program = [
            'prr R1',
            'hlt'
        ];
        let result = assemble(program);
        expect(result).to.deep.equal(['00010a', '000001']);
    });
});
  1. 编写虚拟机器的单元测试。
import { expect } from 'chai';
import VirtualMachine from './vm.js';
describe('VirtualMachine', () => {
    it('should execute a simple program correctly', () => {
        let vm = new VirtualMachine();
        let program = [
            'ldc R0 123',
            'prr R0',
            'hlt'
        ];
        vm.initialize(program);
        vm.run();
        expect(vm.reg[0]).to.equal(123);
    });
});

通过完成这些练习题和拓展应用,我们可以更深入地理解虚拟机器和汇编语言的工作原理,提高编程能力和解决问题的能力。同时,这些知识也为进一步学习计算机体系结构和底层编程打下坚实的基础。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值