“Inline Assembly/Examples”的版本间差异
(创建页面,内容为“What follows is a collection of Inline Assembly functions so common that they should be useful to most OS developers using GCC. Other compilers may have intrinsic alternatives (see references). Notice how these functions are implemented using GNU extensions to the C language and that particular keywords may cause you trouble if you disable GNU extensions. You can still use the disabled keywords such as <tt>asm</tt> if you instead use the alternate keyw…”) |
小 |
||
(未显示同一用户的1个中间版本) | |||
第1行: | 第1行: | ||
以下是一些[[Inline Assembly|内联汇编]]函数的集合,这些函数非常普遍,以至于它们对于使用GCC的大多数操作系统开发人员来说应该是有用的。 其它编译器可能有内在的替代方案(参见参考资料)。 请注意这些函数是如何使用[[C]]语言的GNU扩展实现的,如果禁用GNU扩展,特定的关键字可能会给你带来麻烦。 如果你在保留的命名空间中使用替代关键字,例如 <tt>__asm__</tt>,你仍然可以使用禁用的关键字,例如 <tt>asm</tt>。 小心内联汇编:编译器不理解它发出的汇编代码,如果你对编译器乱写,可能会导致罕见的严重错误。 | |||
''' | '''警告!''' | ||
请注意,<tt>asm("");</tt> <small>(无constants,称为 [https://gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html 基本 <tt>asm</tt>])</small> 和 <tt>asm("":);</tt> <small>(空constraints,[https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html 扩展 <tt>asm</tt>] 的形式)</small> 是不一样的! 除其它差异外,在basic<tt>asm</tt>中,必须使用单个百分号“%”作为寄存器前缀,在扩展<tt>asm</tt>中(即使constraints列表为空),也必须在汇编模板字符串中使用“%”作为前缀。 如果你收到类似“Error: bad register name '%%eax'”之类的GCC错误消息,那么你应该在右括号前插入冒号(‘:’)。 扩展 <tt>asm</tt> 通常是首选的,尽管在 [https://gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html#Remarks 有些情况下,基本 <tt>asm</tt> 是必需的]。 还要注意,这个寄存器前缀只适用于AT&T语法汇编,这是gcc的默认值。 | |||
== | ==内存访问== | ||
=== FAR_PEEKx === | === FAR_PEEKx === | ||
使用默认C data segment以外的另一段读取给定内存位置的8/16/32位值。 不幸的是,直接操作段寄存器没有constraint, 因此,需要手动发布<tt>mov<reg>,<segmentreg></tt>。 | |||
<source lang="c"> | <source lang="c"> | ||
第27行: | 第27行: | ||
=== FAR_POKEx === | === FAR_POKEx === | ||
将8/16/ | 将8/16/32位值写入segment:offset地址。 请注意,与farpeek非常相似,farpoke的这个版本保存并恢复用于访问的段寄存器。 | ||
<source lang="c"> | <source lang="c"> | ||
第45行: | 第45行: | ||
=== OUTx === | === OUTx === | ||
在I/O位置发送8/16/32位值。 | 在I/O位置发送8/16/32位值。 传统命名为<tt>outb</tt>、<tt>outu</tt>和<tt>outl</tt>。 <tt>a</tt>修饰符强制在发出ASM命令之前将val放在eax寄存器中,而<tt>Nd</tt>允许将一个字节的常量值组装为常量,从而释放edX寄存器以供其它情况使用。 | ||
<source lang="c"> | <source lang="c"> | ||
第60行: | 第60行: | ||
=== INx === | === INx === | ||
从I/O位置接收8/16/ | 从I/O位置接收8/16/32位值。 传统命名为<tt>inb</tt>、<tt>inw</tt>和<tt>inl</tt>。 | ||
<source lang="c"> | <source lang="c"> | ||
第77行: | 第77行: | ||
等待很短的时间(通常为1到4微秒)。 用于在旧硬件上实现PIC重新映射的小延迟,或通常作为简单但不精确的等待。 | 等待很短的时间(通常为1到4微秒)。 用于在旧硬件上实现PIC重新映射的小延迟,或通常作为简单但不精确的等待。 | ||
你可以在任何未使用的端口上执行IO操作:Linux内核默认使用端口0x80,该端口通常在POST(上电自检)期间用于在主板的十六进制显示上记录信息,但在引导后几乎不会再使用。 | |||
<source lang="c"> | <source lang="c"> | ||
static inline void io_wait(void) | static inline void io_wait(void) | ||
第85行: | 第85行: | ||
</source> | </source> | ||
== | == 中断相关函数 == | ||
=== | ===启用关闭=== | ||
如果为CPU启用irq,则返回一个<tt>true</tt>布尔值。 | 如果为CPU启用irq,则返回一个<tt>true</tt>布尔值。 | ||
第101行: | 第101行: | ||
} | } | ||
</source> | </source> | ||
=== | === Push/pop出中断标志 === | ||
有时,禁用中断,然后仅当中断以前被禁用时才重新启用它们是有帮助的。 | 有时,禁用中断,然后仅当中断以前被禁用时才重新启用它们是有帮助的。 尽管上述函数也可用于此目的,但以下函数无论IF的状态设置如何,都可以用相同的方式进行: | ||
<source lang="c"> | <source lang="c"> | ||
第125行: | 第125行: | ||
</source> | </source> | ||
Memory clobber强制排序,cc clobber,因为在第二个示例中,所有条件代码都被覆盖。 | Memory clobber强制排序,cc clobber,因为在第二个示例中,所有条件代码都被覆盖。 在第二种情况下没有volatile,因为它没有输出,因此总是volatile的。 | ||
=== LIDT === | === LIDT === | ||
第143行: | 第143行: | ||
</source> | </source> | ||
== | == CPU相关函数 == | ||
=== CPUID === | === CPUID === | ||
请求CPU标识。 有关更多信息,请参见[[CPUID]]。 | |||
<source lang="c"> | <source lang="c"> | ||
第172行: | 第172行: | ||
这可以用来找出执行某些功能所需的时间,对于测试/基准测试等非常有用。 注意:这只是一个近似值。 | 这可以用来找出执行某些功能所需的时间,对于测试/基准测试等非常有用。 注意:这只是一个近似值。 | ||
在x86_64上,“A”constraint期望写入“rdx:rax”寄存器,而不是“edx:eax”。 因此,GCC实际上可以通过根本不设置“rdx”来优化上述代码。 相反,你需要使用位移位手动执行此操作: | |||
<source lang="c"> | <source lang="c"> | ||
第183行: | 第183行: | ||
</source> | </source> | ||
有关更多详细信息,请参阅[https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html#Machine-Constraints GCC Inline Assembly Machine Constraints]。 | |||
=== READ_CRx === | === READ_CRx === | ||
第212行: | 第212行: | ||
=== WRMSR === | === WRMSR === | ||
将64位值写入MSR。 <tt>A</tt>constraint代表寄存器EAX和EDX的串联。 | |||
<source lang="c"> | <source lang="c"> | ||
第221行: | 第221行: | ||
</source> | </source> | ||
在x86_64上,这需要是: | |||
<source lang="c"> | <source lang="c"> | ||
第238行: | 第238行: | ||
=== RDMSR === | === RDMSR === | ||
从MSR读取64位值。 | 从MSR读取64位值。 <tt>A</tt>constraint表示寄存器EAX和EDX的串联。 | ||
<source lang="c"> | <source lang="c"> |
2022年2月22日 (二) 03:10的最新版本
以下是一些内联汇编函数的集合,这些函数非常普遍,以至于它们对于使用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;
}