Inline Assembly/Examples
以下是一些内联汇编函数的集合,这些函数非常普遍,以至于它们对于使用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位值。 传统命名为outb、outu和outl。 a修饰符强制在发出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位值。 传统命名为inb、inw和inl。
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;
}