C 语言、汇编混合编程

在操作系统开发过程中,C 语言本身只能完成一些纯计算型的任务,对于硬件相关的操作,则必须借助汇编语言实现。一般而言,要在项目中引入汇编语言进行开发,主要有内联汇编和独立汇编两种方式。 内联汇编可以直接将汇编代码块嵌入 C 语言代码,直接使用由 C 语言定义的符号,并且在一般情况下无需手动处理调用约定等 ABI 相关的问题。 内联汇编的缺陷在于可移植性较差,不同的编译器对内联汇编的支持各不相同,且语法差异较大。 独立汇编则是单独编写汇编代码,并通过汇编器将其编译为目标文件,再与 C 语言代码链接在一起。 独立汇编的可移植性较好(尤其是使用像 NASM 这样的汇编器时),但需要手动处理调用约定等 ABI 相关的问题。

MSVC 扩展的内联汇编

MSVC 提供的内联汇编采用 Intel 语法,形式上与单独编写的汇编代码十分相似,可读性较好。 使用 Clang 时通过 -fms-extensions 开启 MSVC 扩展后,即可使用 MSVC 风格的内联汇编。 根据微软官方文档的表述,除了 spbp 这两个与栈帧相关的寄存器外,使用其余的通用寄存器时无需手动保存和恢复寄存器的值,编译器会自动处理。

一般情况下,应尽量优先使用 axcxdx 这三个寄存器进行数据传递,避免使用 bxsidi 这三个寄存器,以免与编译器的优化策略冲突。

下面的代码演示了如何使用 MSVC 风格的内联汇编实现一个原子交换操作:

#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>
#include <time.h>

int main()
{
    // 生成 [0, 5) 范围内的随机数
    auto t = time(nullptr);
    srand(t);
    uint32_t number = rand() % 3;
    printf("number = %" PRIu32 "\n", number);
    uint32_t desired = 0;
    printf("desired = %" PRIu32 "\n", desired);
    // 原子交换测试
    bool success = false;
    __asm
    {
        mov eax, desired
        lock cmpxchg dword ptr [number], eax
        jnz fail
        mov success, 1
    fail:
    }
    if (success)
    {
        puts("atomic exchange succeeded");
        return EXIT_SUCCESS;
    }
    else
    {
        puts("atomic exchange failed");
        return EXIT_FAILURE;
    }
}
clang atomic_exchange.c -fms-extensions -o atomic_exchange -std=c23

独立汇编

使用独立汇编时,需要手动处理调用约定等 ABI 相关的问题,尤其是调用约定相关的规则。

popcnt 指令用于计算一个整数的二进制表示中值为 1 的位的数量,属于 x86 指令集中的一条位操作指令。 有了这条指令,可以高效地实现一些需要计算位数的算法,尤其是对位图管理操作。 该指令属于 SSE4.2 指令集扩展的一部分,因此在使用该指令前,必须确保目标 CPU 支持 SSE4.2 指令集。 不幸的是,SimpleOS 运行的环境是 QEMU 模拟的 i386 CPU,因此不支持这条指令。 下面的代码演示了如何使用 MSVC 风格的内联汇编实现 popcnt 指令,其调用遵循 cdecl 调用约定,参数在栈上传递,返回值存放在 eax 寄存器中。

; popcnt.asm
global popcnt
popcnt:
        xor eax, eax
        mov edx, [esp + 4] ; 获取参数
    count:
        test edx, 0xFFFFFFFF ; 若 edx != 0,一直右移到 edx 变成 0
        jz end
        shr edx, 1 ; 右移 1 位,CF 存放移出的位
        jc increment ; 移出的位为 1
        jmp count
    increment:
        inc eax
        jmp count ; 继续统计
    end: ; eax 中存放结果
        ret
// popcnt_test.c
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>

extern __cdecl uint32_t popcnt(uint32_t x);

int main()
{
    uint32_t number = 0b10110101;
    uint32_t count = popcnt(number);
    printf("number = %" PRIu32 ", popcnt = %" PRIu32 "\n", number, count);
    return 0;
}
nasm -f elf32 -o popcnt.obj popcnt.asm
clang popcnt_test.c popcnt.obj -std=c23 -m32 -o popcnt_test