Inline Assembly/Examples

来自osdev
跳到导航 跳到搜索

以下是一些内联汇编函数的集合,这些函数非常普遍,以至于它们对于使用GCC的大多数操作系统开发人员来说应该是有用的。 其它编译器可能有内在的替代方案(参见参考资料)。 请注意这些函数是如何使用C语言的GNU扩展实现的,如果禁用GNU扩展,特定的关键字可能会给你带来麻烦。 如果你在保留的命名空间中使用替代关键字,例如 __asm__,你仍然可以使用禁用的关键字,例如 asm。 小心内联汇编:编译器不理解它发出的汇编代码,如果你对编译器乱写,可能会导致罕见的严重错误。

警告!

请注意,asm(""); (无constants,称为 基本 asm)asm("":); (空constraints,扩展 asm 的形式) 是不一样的! 除其它差异外,在basicasm中,必须使用单个百分号“%”作为寄存器前缀,在扩展asm中(即使constraints列表为空),也必须在汇编模板字符串中使用“%”作为前缀。 如果你收到类似“Error: bad register name '%%eax'”之类的GCC错误消息,那么你应该在右括号前插入冒号(‘:’)。 扩展 asm 通常是首选的,尽管在 有些情况下,基本 asm 是必需的。 还要注意,这个寄存器前缀只适用于AT&T语法汇编,这是gcc的默认值。


内存访问

FAR_PEEKx

使用默认C data segment以外的另一段读取给定内存位置的8/16/32位值。 不幸的是,直接操作段寄存器没有constraint, 因此,需要手动发布mov<reg>,<segmentreg>

static inline uint32_t farpeekl(uint16_t sel, void* off)
{
    uint32_t ret;
    asm ( "push %%fs\n\t"
          "mov  %1, %%fs\n\t"
          "mov  %%fs:(%2), %0\n\t"
          "pop  %%fs"
          : "=r"(ret) : "g"(sel), "r"(off) );
    return ret;
}

FAR_POKEx

将8/16/32位值写入segment:offset地址。 请注意,与farpeek非常相似,farpoke的这个版本保存并恢复用于访问的段寄存器。

static inline void farpokeb(uint16_t sel, void* off, uint8_t v)
{
    asm ( "push %%fs\n\t"
          "mov  %0, %%fs\n\t"
          "movb %2, %%fs:(%1)\n\t"
          "pop %%fs"
          : : "g"(sel), "r"(off), "r"(v) );
    /* TODO: Should "memory" be in the clobber list here? */
}

I/O access

OUTx

在I/O位置发送8/16/32位值。 传统命名为outboutuoutla修饰符强制在发出ASM命令之前将val放在eax寄存器中,而Nd允许将一个字节的常量值组装为常量,从而释放edX寄存器以供其它情况使用。

static inline void outb(uint16_t port, uint8_t val)
{
    asm volatile ( "outb %0, %1" : : "a"(val), "Nd"(port) );
    /* There's an outb %al, $imm8  encoding, for compile-time constant port numbers that fit in 8b.  (N constraint).
     * Wider immediate constants would be truncated at assemble-time (e.g. "i" constraint).
     * The  outb  %al, %dx  encoding is the only option for all other cases.
     * %1 expands to %dx because  port  is a uint16_t.  %w1 could be used if we had the port number a wider C type */
}

INx

从I/O位置接收8/16/32位值。 传统命名为inbinwinl

static inline uint8_t inb(uint16_t port)
{
    uint8_t ret;
    asm volatile ( "inb %1, %0"
                   : "=a"(ret)
                   : "Nd"(port) );
    return ret;
}

IO_WAIT

等待很短的时间(通常为1到4微秒)。 用于在旧硬件上实现PIC重新映射的小延迟,或通常作为简单但不精确的等待。

你可以在任何未使用的端口上执行IO操作:Linux内核默认使用端口0x80,该端口通常在POST(上电自检)期间用于在主板的十六进制显示上记录信息,但在引导后几乎不会再使用。

static inline void io_wait(void)
{
    outb(0x80, 0);
}

中断相关函数

启用关闭

如果为CPU启用irq,则返回一个true布尔值。

static inline bool are_interrupts_enabled()
{
    unsigned long flags;
    asm volatile ( "pushf\n\t"
                   "pop %0"
                   : "=g"(flags) );
    return flags & (1 << 9);
}

Push/pop出中断标志

有时,禁用中断,然后仅当中断以前被禁用时才重新启用它们是有帮助的。 尽管上述函数也可用于此目的,但以下函数无论IF的状态设置如何,都可以用相同的方式进行:

static inline unsigned long save_irqdisable(void)
{
    unsigned long flags;
    asm volatile ("pushf\n\tcli\n\tpop %0" : "=r"(flags) : : "memory");
    return flags;
}

static inline void irqrestore(unsigned long flags)
{
asm ("push %0\n\tpopf" : : "rm"(flags) : "memory","cc");
}

static void intended_usage(void)
{
unsigned long f = save_irqdisable();
do_whatever_without_irqs();
irqrestore(f);
}

Memory clobber强制排序,cc clobber,因为在第二个示例中,所有条件代码都被覆盖。 在第二种情况下没有volatile,因为它没有输出,因此总是volatile的。

LIDT

定义一个新的中断表。

static inline void lidt(void* base, uint16_t size)
{   // This function works in 32 and 64bit mode
    struct {
        uint16_t length;
        void*    base;
    } __attribute__((packed)) IDTR = { size, base };

    asm ( "lidt %0" : : "m"(IDTR) );  // let the compiler choose an addressing mode
}

CPU相关函数

CPUID

请求CPU标识。 有关更多信息,请参见CPUID

/* GCC has a <cpuid.h> header you should use instead of this. */
static inline void cpuid(int code, uint32_t* a, uint32_t* d)
{
    asm volatile ( "cpuid" : "=a"(*a), "=d"(*d) : "0"(code) : "ebx", "ecx" );
}

RDTSC

读取CPU时间戳计数器的当前值并存储到EDX:EAX中。 时间戳计数器包含自上次CPU重置以来经过的时钟滴答数。 该值存储在64位MSR中,并在每个时钟周期后递增。

static inline uint64_t rdtsc()
{
    uint64_t ret;
    asm volatile ( "rdtsc" : "=A"(ret) );
    return ret;
}

这可以用来找出执行某些功能所需的时间,对于测试/基准测试等非常有用。 注意:这只是一个近似值。

在x86_64上,“A”constraint期望写入“rdx:rax”寄存器,而不是“edx:eax”。 因此,GCC实际上可以通过根本不设置“rdx”来优化上述代码。 相反,你需要使用位移位手动执行此操作:

uint64_t rdtsc(void)
{
    uint32_t low, high;
    asm volatile("rdtsc":"=a"(low),"=d"(high));
    return ((uint64_t)high << 32) | low;
}

有关更多详细信息,请参阅GCC Inline Assembly Machine Constraints

READ_CRx

读取控制寄存器中的值。

static inline unsigned long read_cr0(void)
{
    unsigned long val;
    asm volatile ( "mov %%cr0, %0" : "=r"(val) );
    return val;
}

INVLPG

使一个特定虚拟地址的TLB(转换查找缓冲区)无效。 页面的下一个内存引用将被强制从主内存重新读取PDE和PTE。 必须在每次更新其中一个表时发出。 m指针指向逻辑地址,而不是物理或虚拟地址:ds段的偏移量。

static inline void invlpg(void* m)
{
    /* Clobber memory to avoid optimizer re-ordering access before invlpg, which may cause nasty bugs. */
    asm volatile ( "invlpg (%0)" : : "b"(m) : "memory" );
}

WRMSR

将64位值写入MSR。 Aconstraint代表寄存器EAX和EDX的串联。

static inline void wrmsr(uint32_t msr_id, uint64_t msr_value)
{
    asm volatile ( "wrmsr" : : "c" (msr_id), "A" (msr_value) );
}

在x86_64上,这需要是:

static inline void wrmsr(uint64_t msr, uint64_t value)
{
	uint32_t low = value & 0xFFFFFFFF;
	uint32_t high = value >> 32;
	asm volatile (
		"wrmsr"
		:
		: "c"(msr), "a"(low), "d"(high)
	);
}

RDMSR

从MSR读取64位值。 Aconstraint表示寄存器EAX和EDX的串联。

static inline uint64_t rdmsr(uint32_t msr_id)
{
    uint64_t msr_value;
    asm volatile ( "rdmsr" : "=A" (msr_value) : "c" (msr_id) );
    return msr_value;
}

在x86_64上,这需要:

static inline uint64_t rdmsr(uint64_t msr)
{
	uint32_t low, high;
	asm volatile (
		"rdmsr"
		: "=a"(low), "=d"(high)
		: "c"(msr)
	);
	return ((uint64_t)high << 32) | low;
}