位运算与硬件操作 (Bitwise Operations & Hardware Interaction)


章节概述

位运算是 C 语言最接近硬件的特性之一。所有高级语言的布尔运算、乘除法优化、加密算法,最终都翻译为 CPU 的 AND/OR/XOR/SHIFT 指令。本章从 CPU 的逻辑门出发,深入位运算符的底层实现,然后延伸到 volatile 关键字、内存映射 I/O(MMIO)和 GCC 内联汇编——这些是嵌入式开发和操作系统内核的必备技能。建议同时阅读 中的位操作指令和 中的 MMIO。

位运算之所以重要,不只是因为它们是高效的——有些操作只有位运算才能实现。标志位的设置、硬件寄存器的操控、协议头的解析、加密算法,都依赖精确到位的操作。

本章知识地图

graph TD
    A["位运算与硬件"] --> B["位运算符: & | ^ ~ << >>"]
    A --> C["位掩码技术"]
    A --> D["位域 struct"]
    A --> E["volatile 关键字"]
    A --> F["MMIO 内存映射I/O"]
    A --> G["内联汇编 (GCC)"]
    B --> H["汇编视角: AND OR XOR SHL SHR TEST"]
    C --> I["set/clear/toggle/test bits"]
    E --> J["硬件寄存器访问"]
    F --> J
    G --> J

第一节: 六大位运算符深度解析


1.1 位运算真值表

C 语言提供六种位运算符,它们逐位操作整型操作数:

运算符名称真值表
&按位与1&1=1, 1&0=0, 0&1=0, 0&0=0
|按位或1|1=1, 1|0=1, 0|1=1, 0|0=0
^按位异或1^1=0, 1^0=1, 0^1=1, 0^0=0
~按位取反~1=0, ~0=1 (一元运算符)
<<左移左移 n 位,右侧补 0
>>右移右移 n 位(无符号数补0,有符号数实现定义)
unsigned char a = 0b11001100;   // 204
unsigned char b = 0b10101010;   // 170
 
printf("a & b  = 0x%02X\n", a & b);   // 0x88 = 0b10001000
printf("a | b  = 0x%02X\n", a | b);   // 0xEE = 0b11101110
printf("a ^ b  = 0x%02X\n", a ^ b);   // 0x66 = 0b01100110
printf("~a     = 0x%02X\n", (unsigned char)~a); // 0x33 = 0b00110011
printf("a << 1 = 0x%02X\n", a << 1);  // 0x98 = 0b10011000
printf("b >> 2 = 0x%02X\n", b >> 2);  // 0x2A = 0b00101010

1.2 汇编视角——CPU 的位操作指令

C 语言的位运算符几乎直接对应 CPU 指令:

unsigned int x = 0x0F;
unsigned int y = 0x36;
unsigned int r;
 
r = x & y;   // r = 0x06
r = x | y;   // r = 0x3F
r = x ^ y;   // r = 0x39
r = ~x;      // r = 0xFFFFFFF0 (32位取反)
r = x << 2;  // r = 0x3C
r = y >> 1;  // r = 0x1B

对应的 x86-64 汇编:

# r = x & y;
movl    -4(%rbp), %eax      # 加载 x
andl    -8(%rbp), %eax      # AND 指令: eax = eax & y
movl    %eax, -12(%rbp)     # 存入 r
 
# r = x | y;
movl    -4(%rbp), %eax
orl     -8(%rbp), %eax      # OR 指令
 
# r = x ^ y;
movl    -4(%rbp), %eax
xorl    -8(%rbp), %eax      # XOR 指令
 
# r = ~x;
movl    -4(%rbp), %eax
notl    %eax                 # NOT 指令
 
# r = x << 2;
movl    -4(%rbp), %eax
sall    $2, %eax             # SHL (Shift Arithmetic Left) 指令
 
# r = y >> 1;
movl    -8(%rbp), %eax
shrl    $1, %eax             # SHR (Shift Logical Right) 指令

TEST 指令的特殊性: testl %eax, %eaxandl %eax, %eax只影响标志位,不修改目标寄存器。编译器用 TEST 实现 if (x & mask) 判断,避免破坏 x 的值。

1.3 有符号数右移的陷阱

int x = -8;           // 二进制补码: ...11111000
int y = x >> 2;       // 实现定义!GCC/Clang: 算术右移 → -2
                      // 某些平台: 逻辑右移 → 一个很大的正数
 
unsigned int ux = (unsigned int)-8;  // 0xFFFFFFF8
unsigned int uy = ux >> 2;           // 逻辑右移 → 0x3FFFFFFE
// 无符号数右移一定是逻辑右移(补0),这是标准保证的!
 
printf("有符号右移: %d\n", y);    // GCC 上: -2
printf("无符号右移: %u\n", uy);   // 1073741822
graph TD
    subgraph "有符号数 -8 (0xF8)"
        S_BITS["11111000"]
    end
    subgraph "算术右移 >>2 (= -2)"
        A_BITS["11111110"]
    end
    subgraph "逻辑右移 >>2"
        L_BITS["00111110"]
    end
    S_BITS -->|"算术右移<br/>(补符号位)"| A_BITS
    S_BITS -->|"逻辑右移<br/>(补0)"| L_BITS

最佳实践: 位运算总是使用无符号类型 (unsigned int, uint32_t 等)。有符号数的位操作是实现定义的,不可移植。


第二节: 位掩码——位的操控艺术


2.1 位掩码的四种基本操作

#include <stdint.h>
 
// 设定位 (Set): 将指定位设为 1
#define SET_BIT(reg, bit)    ((reg) |= (1U << (bit)))
 
// 清除位 (Clear): 将指定位设为 0
#define CLEAR_BIT(reg, bit)  ((reg) &= ~(1U << (bit)))
 
// 翻转位 (Toggle): 反转指定位
#define TOGGLE_BIT(reg, bit) ((reg) ^= (1U << (bit)))
 
// 测试位 (Test): 检查指定位是否为 1
#define TEST_BIT(reg, bit)   (((reg) >> (bit)) & 1U)
 
// 获取多位域的值
#define GET_FIELD(reg, mask, shift)  (((reg) & (mask)) >> (shift))
 
// 设置多位域的值
#define SET_FIELD(reg, mask, shift, val) \
    ((reg) = ((reg) & ~(mask)) | (((val) << (shift)) & (mask)))
uint32_t flags = 0;
 
// 演示每种操作
SET_BIT(flags, 3);        // flags: ...00001000
SET_BIT(flags, 7);        // flags: ...10001000
printf("After set:   0x%08X\n", flags);
 
TOGGLE_BIT(flags, 3);     // flags: ...10000000 (位3从1变0)
printf("After toggle: 0x%08X\n", flags);
 
printf("Bit 7 is %s\n", TEST_BIT(flags, 7) ? "set" : "not set");
printf("Bit 3 is %s\n", TEST_BIT(flags, 3) ? "set" : "not set");
 
CLEAR_BIT(flags, 7);      // flags: ...00000000
printf("After clear:   0x%08X\n", flags);

2.2 实用位操作技巧

// 判断是否为 2 的幂
// 如果 x 是 2 的幂,x & (x-1) == 0(且 x != 0)
int is_power_of_two(unsigned int x) {
    return x && !(x & (x - 1));
}
// 原理: 8 = 00001000, 7 = 00000111, 8 & 7 = 0
 
// 计算二进制中 1 的个数 (Brian Kernighan 算法)
int popcount(unsigned int x) {
    int count = 0;
    while (x) {
        x &= x - 1;   // 清除最低位的 1
        count++;
    }
    return count;
}
// 每次循环: x = x & (x-1) 消除最低的一个 1 位
 
// 取最低位的 1
#define LOWBIT(x) ((x) & (-(x)))
// 原理: -x = ~x + 1, x & (-x) 只保留最低位的 1
// 例如 x=0b101100, -x=0b010100, x & -x = 0b000100
 
// 对齐到 2 的幂的倍数 (向上对齐)
size_t align_up(size_t x, size_t alignment) {
    return (x + alignment - 1) & ~(alignment - 1);
}
// 例如 align_up(13, 8) = 16, align_up(17, 16) = 32
 
// 交换两个数(不用临时变量)
void swap_xor(int *a, int *b) {
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}
// 原理: a'=a^b, b'=b^(a^b)=a, a''=(a^b)^a=b
// ️ 注意: a 和 b 指向同一地址时会出bug

2.3 汇编对应——按位测试

// C 语言测试位
if (flags & (1U << 5)) {
    // 执行某操作
}
# 汇编实现 (GCC)
movl    flags(%rip), %eax    # 加载 flags
testl   $32, %eax            # flags & 0x20 (1<<5)
je      .L_skip              # 如果为零跳转
# 执行某操作的代码
.L_skip:

test 指令比 and 更好——只设置标志位,不修改原值。这正是 TEST_BIT 宏应该生成的内容。


第三节: 结构体位域


3.1 位域的语法

位域允许在结构体中按位定义成员,对于打包标志和解析硬件寄存器位非常有用:

#include <stdio.h>
#include <stdint.h>
 
// 32 位控制寄存器
struct ControlReg {
    uint32_t enable     : 1;   // 位 0: 使能
    uint32_t mode       : 2;   // 位 1-2: 模式 (0-3)
    uint32_t interrupt  : 1;   // 位 3: 中断使能
    uint32_t reserved   : 4;   // 位 4-7: 保留
    uint32_t baud_rate  : 8;   // 位 8-15: 波特率分频
    uint32_t flags      : 8;   // 位 16-23: 标志位
    uint32_t version    : 8;   // 位 24-31: 版本号
};
 
int main() {
    struct ControlReg reg = {0};
    reg.enable = 1;
    reg.mode = 2;             // 0b10
    reg.interrupt = 1;
    reg.baud_rate = 52;       // 最大值 255 (8位)
    reg.version = 0x12;
 
    printf("sizeof(reg) = %zu\n", sizeof(reg));  // 通常 4 (32位)
 
    // 获取整个寄存器的值
    uint32_t *reg_val = (uint32_t*)&reg;
    printf("寄存器值 = 0x%08X\n", *reg_val);
    return 0;
}
graph LR
    subgraph "32位控制寄存器"
        B0["enable<br/>bit 0"]
        B1["mode[0]<br/>bit 1"]
        B2["mode[1]<br/>bit 2"]
        B3["interrupt<br/>bit 3"]
        B4["reserved[0]<br/>bit 4"]
        B5["reserved[1]<br/>bit 5"]
        B6["reserved[2]<br/>bit 6"]
        B7["reserved[3]<br/>bit 7"]
        B8["baud_rate[0]<br/>bit 8"]
        B15["baud_rate[7]<br/>bit 15"]
        B16["flags...<br/>bit 16-23"]
        B24["version...<br/>bit 24-31"]
    end

3.2 位域的局限和陷阱

// ️ 位域的可移植性问题
 
// 1. 布局顺序依赖字节序和编译器
struct Flags {
    unsigned int a : 1;
    unsigned int b : 1;
    unsigned int c : 1;
};
// 小端机器: a 在最低位; 大端机器: a 在最高位
// 不同编译器可能有不同的填充策略
 
// 2. 不能对位域取地址
// &reg.enable  // 编译错误! 位域没有独立地址
 
// 3. 跨越类型的位域
struct Mixed {
    uint32_t a : 16;
    uint32_t b : 16;
    // 如果改用 uint8_t c : 8; 可能强制新的存储单元
};
 
// 4. 性能开销: 读写位域通常需要读-改-写周期
reg.enable = 0;
// 编译为: 读取整个32位值 → 清除bit0 → 写回32位值
// 这比单纯的位运算宏慢,但代码更可读

位域 vs 位掩码宏: 位域可读性更好,适合协议解析和配置结构;位掩码宏具有确定性的内存布局,适合硬件寄存器操作和跨平台代码。


第四节: volatile 关键字与硬件寄存器


4.1 volatile 的语义

volatile 告诉编译器:每次访问这个变量都必须从内存读取/写入,不要优化到寄存器中。这是访问硬件寄存器的必备关键字。

// volatile 的三种典型场景:
 
// 1. 内存映射 I/O 寄存器
volatile uint32_t *uart_status = (volatile uint32_t *)0x4000C000;
while (!(*uart_status & 0x20)) {
    // 等待 UART 发送就绪
    // 不加 volatile: 编译器可能优化为只读一次,死循环!
}
 
// 2. 信号处理器中的标志
volatile sig_atomic_t flag = 0;
void handler(int sig) { flag = 1; }
 
// 3. setjmp/longjmp 中的变量
volatile int progress = 0;
// 实验: 验证 volatile 对生成代码的影响
 
int normal = 0;
volatile int vol = 0;
 
void test_loop() {
    while (normal == 0) { /* do work */ }
    // 编译器优化后: 可能只读一次 normal,变成死循环或直接跳过
 
    while (vol == 0) { /* do work */ }
    // 编译器: 每次循环都从内存读取 vol
}

生成的汇编对比:

# while (normal == 0) — 糟糕的优化
    movl    normal(%rip), %eax    # 只加载一次
.L_loop1:
    testl   %eax, %eax            # 测试寄存器中的值(永远不变)
    je      .L_loop1              # 死循环!
 
# while (vol == 0) — 正确的代码
.L_loop2:
    movl    vol(%rip), %eax       # 每次循环都重新加载
    testl   %eax, %eax
    je      .L_loop2              # 可以退出

4.2 volatile 不提供的保证

// volatile 不是多线程同步原语!
volatile int counter = 0;
 
// 线程 A
void thread_a(void) {
    counter++;   // 不是原子操作!
    // counter++ 实际是: 读 → 加1 → 写 (三个指令)
    // 线程 B 可能在这之间读取到中间状态
}
 
// 需要原子操作时使用 C11 _Atomic
#include <stdatomic.h>
atomic_int safe_counter = 0;
// safe_counter++ 是原子操作 (lock xaddl 指令)

volatile_Atomic。volatile 只禁用编译器优化,不提供原子性。详见 C11 标准的 <stdatomic.h>


第五节: 内存映射 I/O (MMIO) 基础


5.1 MMIO 的概念

CPU 通过内存地址访问外设寄存器——这些不是真正的 RAM 内存,而是映射到外设硬件寄存器的地址空间。对某个地址的读/写操作被总线路由到相应的外设控制器。

graph TD
    CPU["CPU"] -->|"LOAD/STORE 指令<br/>访问特定地址"| BUS["系统总线"]
    BUS --> RAM["RAM<br/>真正的内存"]
    BUS --> UART["UART 控制器<br/>0x4000C000"]
    BUS --> GPIO["GPIO 控制器<br/>0x40020000"]
    BUS --> TIMER["定时器<br/>0x40010000"]
// 典型 MMIO 用法 (以 STM32 伪代码为例)
 
// 定义外设基地址
#define UART_BASE    0x4000C000UL
#define GPIOA_BASE   0x40020000UL
#define TIM2_BASE    0x40000000UL
 
// 定义寄存器指针 (volatile 必须!)
#define UART_SR      ((volatile uint32_t *)(UART_BASE + 0x00))  // 状态寄存器
#define UART_DR      ((volatile uint32_t *)(UART_BASE + 0x04))  // 数据寄存器
#define UART_BRR     ((volatile uint32_t *)(UART_BASE + 0x08))  // 波特率寄存器
#define UART_CR1     ((volatile uint32_t *)(UART_BASE + 0x0C))  // 控制寄存器1
 
// 初始化 UART
void uart_init(uint32_t baud) {
    *UART_BRR = SystemCoreClock / baud;
    *UART_CR1 |= (1 << 3)   // 发送使能
              |  (1 << 2)   // 接收使能
              |  (1 << 13); // UART 使能
}
 
// 发送字符
void uart_putc(char c) {
    // 等待发送寄存器空 (TXE 位)
    while (!(*UART_SR & (1 << 7)))
        ;  // 轮询
 
    *UART_DR = c;  // 写入数据寄存器
}
 
// 接收字符
char uart_getc(void) {
    // 等待接收非空 (RXNE 位)
    while (!(*UART_SR & (1 << 5)))
        ;  // 轮询
 
    return (char)(*UART_DR & 0xFF);
}

5.2 MMIO 在 Linux 上的演示

// 在 Linux 用户空间演示 MMIO 概念
// 使用 /dev/mem 访问物理地址(需要 root 权限)
// 或使用 mmap 映射硬件寄存器区域
 
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
 
// 模拟的硬件寄存器地址(实际不会工作,仅为概念演示)
#define FAKE_REG_BASE 0x10000000
 
int main() {
    int fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (fd < 0) {
        perror("open /dev/mem (需要 root)");
        return 1;
    }
 
    // 将物理地址映射到进程虚拟地址空间
    volatile uint32_t *reg = mmap(
        NULL, 4096,
        PROT_READ | PROT_WRITE,
        MAP_SHARED,
        fd, FAKE_REG_BASE
    );
 
    if (reg == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }
 
    // 读寄存器
    uint32_t val = reg[0];
    printf("寄存器[0] = 0x%08X\n", val);
 
    // 写寄存器
    reg[1] = 0xDEADBEEF;
 
    munmap((void*)reg, 4096);
    close(fd);
    return 0;
}

安全警告: 在真实系统中使用 /dev/mem 操作物理内存极度危险——可能损坏硬件、导致系统崩溃。上述代码仅在嵌入式开发板或虚拟机中用于教学目的。


第六节: GCC 内联汇编入门


6.1 基础语法

内联汇编允许在 C 代码中直接插入汇编指令,用于无法用纯 C 表达的操作(如读取特定寄存器、执行特殊指令):

// GCC 扩展内联汇编语法:
// asm volatile(
//     "指令模板"
//     : 输出操作数列表
//     : 输入操作数列表
//     : 破坏(clobber)列表
// );
#include <stdio.h>
#include <stdint.h>
 
// 1. 读取 CPU 时间戳计数器 (RDTSC 指令,仅 x86)
uint64_t read_tsc(void) {
    uint32_t low, high;
    asm volatile (
        "rdtsc"
        : "=a" (low), "=d" (high)    // 输出: eax → low, edx → high
    );
    return ((uint64_t)high << 32) | low;
}
 
// 2. 内联 NOP (空操作)
void nop_example(void) {
    asm volatile ("nop");    // 插入一个 NOP 指令
}
 
// 3. 读取栈指针 (RSP)
void *get_stack_pointer(void) {
    void *sp;
    asm volatile (
        "movq %%rsp, %0"
        : "=r" (sp)    // 输出到任意通用寄存器
    );
    return sp;
}
 
// 4. 内存屏障 (防止编译器重排内存访问)
#define barrier() asm volatile ("" ::: "memory")
 
// 5. 禁用/启用中断 (内核代码,仅示例)
static inline void cli(void) {
    asm volatile ("cli" ::: "memory");
}
static inline void sti(void) {
    asm volatile ("sti" ::: "memory");
}
 
int main() {
    printf("栈指针: %p\n", get_stack_pointer());
    printf("时间戳: %lu\n", read_tsc());
    return 0;
}

6.2 约束(Constraints)说明

约束含义架构
"r"任意通用寄存器通用
"a"eax/rax 寄存器x86
"b"ebx/rbx 寄存器x86
"c"ecx/rcx 寄存器x86
"d"edx/rdx 寄存器x86
"m"内存操作数通用
"i"立即数通用
"=r"输出到寄存器通用
"+r"输入/输出寄存器通用
// 约束组合示例: 原子加法
int atomic_add(volatile int *ptr, int val) {
    int result;
    asm volatile (
        "lock; xaddl %0, %1"     // lock 前缀保证原子性
        : "=r" (result), "+m" (*ptr)
        : "0" (val)
        : "memory"
    );
    return result;
}

完整的内联汇编教程参见 。


第七节: 综合案例——GPIO 控制 LED


#include <stdint.h>
#include <stdbool.h>
 
// 模拟 STM32 风格的 GPIO 控制
// 实际地址依赖于具体芯片,此处为概念演示
 
#define GPIOA_BASE  0x40020000UL
 
// GPIO 寄存器定义
typedef struct {
    volatile uint32_t MODER;    // 模式寄存器 (偏移 0x00)
    volatile uint32_t OTYPER;   // 输出类型 (偏移 0x04)
    volatile uint32_t OSPEEDR;  // 输出速度 (偏移 0x08)
    volatile uint32_t PUPDR;    // 上下拉 (偏移 0x0C)
    volatile uint32_t IDR;      // 输入数据 (偏移 0x10)
    volatile uint32_t ODR;      // 输出数据 (偏移 0x14)
    volatile uint32_t BSRR;     // 置位/复位 (偏移 0x18)
    volatile uint32_t LCKR;     // 锁定 (偏移 0x1C)
    volatile uint32_t AFRL;     // 复用功能低 (偏移 0x20)
    volatile uint32_t AFRH;     // 复用功能高 (偏移 0x24)
} GPIO_TypeDef;
 
#define GPIOA  ((GPIO_TypeDef *)GPIOA_BASE)
 
// 位掩码辅助宏
#define GPIO_PIN_5  (1U << 5)
 
// 初始化 PA5 为推挽输出 (连接 LED)
void led_init(void) {
    // 清除 MODER[11:10] 位,然后设为 01 (通用输出模式)
    GPIOA->MODER &= ~(0x3U << 10);   // 清除 PA5 模式位
    GPIOA->MODER |=  (0x1U << 10);   // 设为输出模式
 
    // 设输出类型为推挽 (0)
    GPIOA->OTYPER &= ~GPIO_PIN_5;
 
    // 初始输出低电平 (LED 灭)
    GPIOA->ODR &= ~GPIO_PIN_5;
}
 
// LED 亮
void led_on(void) {
    GPIOA->BSRR = GPIO_PIN_5;    // BSRR 写1置位 (原子操作)
}
 
// LED 灭
void led_off(void) {
    GPIOA->BSRR = (GPIO_PIN_5 << 16);  // BSRR高16位写1复位 (原子操作)
}
 
// LED 翻转
void led_toggle(void) {
    GPIOA->ODR ^= GPIO_PIN_5;    // 异或翻转
}
 
// 简单延时 (忙等)
void delay_ms(uint32_t ms) {
    // 实际使用系统定时器; 此处简化
    for (volatile uint32_t i = 0; i < ms * 4000; i++)
        ;
}
 
int main(void) {
    led_init();
 
    while (1) {
        led_on();
        delay_ms(500);
        led_off();
        delay_ms(500);
        // LED 以 1Hz 频率闪烁
    }
    return 0;
}

章节测试

判断题(共10题)

判断题 1

x & (1 << n) 用于测试 x 的第 n 位是否为 1。 ( )

  • 正确

  • 错误

判断题 2

x ^= (1 << n) 的效果是将第 n 位设为 1。 ( )

  • 正确

  • 错误

判断题 3

有符号整数的右移行为是标准 C 定义的。 ( )

  • 正确

  • 错误

判断题 4

volatile 关键字保证多线程访问的原子性。 ( )

  • 正确

  • 错误

判断题 5

结构体位域可以跨不同底层类型存储。 ( )

  • 正确

  • 错误

判断题 6

~0 的值总是 -1。 ( )

  • 正确

  • 错误

判断题 7

asm volatile ("nop"); 在 GCC 内联汇编中插入一个空操作指令。 ( )

  • 正确

  • 错误

判断题 8

MMIO 中的外设寄存器地址与 RAM 地址在物理上是相同的总线访问。 ( )

  • 正确

  • 错误

判断题 9

x & (x - 1) 用于检测 x 是否为 2 的幂(x > 0)。 ( )

  • 正确

  • 错误

判断题 10

位运算 & | ^ 的优先级高于 == !=。 ( )

  • 正确

  • 错误

选择题(共10题)

选择题 1

要将一个字节的 bit3 清零,应使用:

  • A. x |= (1 << 3)

  • B. x &= ~(1 << 3)

  • C. x ^= (1 << 3)

  • D. x &= (1 << 3)

选择题 2

0x0F ^ 0x3C 的结果是:

  • A. 0x3F

  • B. 0x33

  • C. 0x0C

  • D. 0x30

选择题 3

以下哪个指令是 x86 中实现 if (x & mask) 最有效的方式?

  • A. andl mask, %eax

  • B. testl mask, %eax

  • C. cmpl mask, %eax

  • D. xorl mask, %eax

选择题 4

uint32_t x = 0x12345678; x = (x >> 16) | (x << 16); 后 x 的值是:

  • A. 0x12345678

  • B. 0x56781234

  • C. 0x78563412

  • D. 0x34127856

选择题 5

volatile 关键字的正确用途是:

  • A. 优化代码性能

  • B. 防止编译器优化掉对该变量的内存访问

  • C. 保证多线程数据安全

  • D. 将变量放到只读段

选择题 6

位域的主要缺点不包括:

  • A. 不能取地址

  • B. 跨平台布局不一致

  • C. 读-改-写性能开销

  • D. 不能在结构体中使用

选择题 7

x << 3 在正整数上等价于:

  • A. x * 3

  • B. x * 8

  • C. x * 6

  • D. x / 8

选择题 8

GCC 内联汇编中 "=r" 约束的含义是:

  • A. 输入操作数,使用任意通用寄存器

  • B. 输出操作数,使用任意通用寄存器

  • C. 输入操作数,使用 eax 寄存器

  • D. 内存操作数

选择题 9

以下哪个内联汇编约束表示”clobber 内存”?

  • A. "cc"

  • B. "memory"

  • C. "r"

  • D. "=&r"

选择题 10

在 MMIO 中,以下代码为什么可能出错?

uint32_t *gpio_out = (uint32_t *)0x40020014;
*gpio_out |= (1 << 5);  // 设置 bit 5
  • A. 地址错误

  • B. 应该是 volatile 指针

  • C. |= 会执行读-改-写,可能触发副作用

  • D. B 和 C 都是


编程练习题

练习 1:位操作工具库

难度:

实现完整的位操作宏/函数库:

  • set_bits(reg, mask) — 设置掩码指定的位
  • clear_bits(reg, mask) — 清除掩码指定的位
  • toggle_bits(reg, mask) — 翻转掩码指定的位
  • extract_bits(value, offset, width) — 提取位段
  • deposit_bits(value, offset, width, new_val) — 写入位段
  • rotate_left(value, n)rotate_right(value, n) — 循环移位
  • reverse_bits(value) — 位反转
  • 编写单元测试验证每个函数

练习 2:硬件寄存器模拟器

难度:

使用位域实现一个外设寄存器模拟:

  • 定义 UART 控制/状态/数据寄存器结构
  • 实现 uart_init(int baud) 配置
  • 实现 uart_write(const char *data, size_t len) 发送
  • 实现 uart_read(char *buf, size_t len) 接收(用环形缓冲模拟)
  • volatile 标记所有寄存器
  • 不使用操作系统调用,纯内存模拟

练习 3:简易虚拟 CPU

难度:

使用位运算解析和执行指令:

  • 指令格式: [opcode:4bit][reg1:2bit][reg2:2bit][imm:8bit] = 16位
  • 操作码: ADD(0), SUB(1), AND(2), OR(3), XOR(4), MOV(5), LOAD(6), STORE(7)
  • R0-R3 四个通用寄存器
  • 实现指令提取、解码、执行循环
  • 使用位域或位掩码宏提取各字段

知识网络

  • 汇编参考: — AND/OR/XOR/SHL/SHR/TEST 指令
  • 汇编参考: — MMIO 的硬件实现
  • 汇编参考: — 完整的内联汇编教程
  • 同系列相关: 内存模型与布局 — 地址空间与 MMIO 定位
  • 同系列相关: 编译链接与ELF — 链接器脚本中的内存布局