mirror of
https://github.com/RGBCube/serenity
synced 2025-06-01 02:28:12 +00:00

We now have these API's in <Kernel/Random.h>: - get_fast_random_bytes(u8* buffer, size_t buffer_size) - get_good_random_bytes(u8* buffer, size_t buffer_size) - get_fast_random<T>() - get_good_random<T>() Internally they both use x86 RDRAND if available, otherwise they fall back to the same LCG we had in RandomDevice all along. The main purpose of this patch is to give kernel code a way to better express its needs for random data. Randomness is something that will require a lot more work, but this is hopefully a step in the right direction.
548 lines
17 KiB
C++
548 lines
17 KiB
C++
#include "APIC.h"
|
|
#include "Assertions.h"
|
|
#include "IRQHandler.h"
|
|
#include "PIC.h"
|
|
#include "Process.h"
|
|
#include <AK/Types.h>
|
|
#include <Kernel/Arch/i386/CPU.h>
|
|
#include <Kernel/KSyms.h>
|
|
#include <Kernel/VM/MemoryManager.h>
|
|
#include <LibC/mallocdefs.h>
|
|
|
|
//#define PAGE_FAULT_DEBUG
|
|
|
|
struct [[gnu::packed]] DescriptorTablePointer
|
|
{
|
|
u16 limit;
|
|
void* address;
|
|
};
|
|
|
|
static DescriptorTablePointer s_idtr;
|
|
static DescriptorTablePointer s_gdtr;
|
|
static Descriptor s_idt[256];
|
|
static Descriptor s_gdt[256];
|
|
|
|
static IRQHandler* s_irq_handler[16];
|
|
|
|
static Vector<u16>* s_gdt_freelist;
|
|
|
|
static u16 s_gdt_length;
|
|
|
|
u16 gdt_alloc_entry()
|
|
{
|
|
ASSERT(s_gdt_freelist);
|
|
ASSERT(!s_gdt_freelist->is_empty());
|
|
return s_gdt_freelist->take_last();
|
|
}
|
|
|
|
void gdt_free_entry(u16 entry)
|
|
{
|
|
s_gdt_freelist->append(entry);
|
|
}
|
|
|
|
extern "C" void handle_irq(RegisterDump);
|
|
extern "C" void irq_common_asm_entry();
|
|
|
|
#define GENERATE_IRQ_ASM_ENTRY(irq, isr_number) \
|
|
extern "C" void irq_##irq##_asm_entry(); \
|
|
asm(".globl irq_" #irq "_asm_entry\n" \
|
|
"irq_" #irq "_asm_entry:\n" \
|
|
" pushw $" #isr_number "\n" \
|
|
" pushw $0\n" \
|
|
" jmp irq_common_asm_entry\n");
|
|
|
|
asm(
|
|
".globl irq_common_asm_entry\n"
|
|
"irq_common_asm_entry: \n"
|
|
" pusha\n"
|
|
" pushl %ds\n"
|
|
" pushl %es\n"
|
|
" pushl %fs\n"
|
|
" pushl %gs\n"
|
|
" pushl %ss\n"
|
|
" mov $0x10, %ax\n"
|
|
" mov %ax, %ds\n"
|
|
" mov %ax, %es\n"
|
|
" cld\n"
|
|
" call handle_irq\n"
|
|
" add $0x4, %esp\n" // "popl %ss"
|
|
" popl %gs\n"
|
|
" popl %fs\n"
|
|
" popl %es\n"
|
|
" popl %ds\n"
|
|
" popa\n"
|
|
" add $0x4, %esp\n"
|
|
" iret\n");
|
|
|
|
#define EH_ENTRY(ec, title) \
|
|
extern "C" void title##_asm_entry(); \
|
|
extern "C" void title##_handler(RegisterDump); \
|
|
asm( \
|
|
".globl " #title "_asm_entry\n" \
|
|
"" #title "_asm_entry: \n" \
|
|
" pusha\n" \
|
|
" pushl %ds\n" \
|
|
" pushl %es\n" \
|
|
" pushl %fs\n" \
|
|
" pushl %gs\n" \
|
|
" pushl %ss\n" \
|
|
" mov $0x10, %ax\n" \
|
|
" mov %ax, %ds\n" \
|
|
" mov %ax, %es\n" \
|
|
" cld\n" \
|
|
" call " #title "_handler\n" \
|
|
" add $0x4, %esp \n" \
|
|
" popl %gs\n" \
|
|
" popl %fs\n" \
|
|
" popl %es\n" \
|
|
" popl %ds\n" \
|
|
" popa\n" \
|
|
" add $0x4, %esp\n" \
|
|
" iret\n");
|
|
|
|
#define EH_ENTRY_NO_CODE(ec, title) \
|
|
extern "C" void title##_handler(RegisterDump); \
|
|
extern "C" void title##_asm_entry(); \
|
|
asm( \
|
|
".globl " #title "_asm_entry\n" \
|
|
"" #title "_asm_entry: \n" \
|
|
" pushl $0x0\n" \
|
|
" pusha\n" \
|
|
" pushl %ds\n" \
|
|
" pushl %es\n" \
|
|
" pushl %fs\n" \
|
|
" pushl %gs\n" \
|
|
" pushl %ss\n" \
|
|
" mov $0x10, %ax\n" \
|
|
" mov %ax, %ds\n" \
|
|
" mov %ax, %es\n" \
|
|
" cld\n" \
|
|
" call " #title "_handler\n" \
|
|
" add $0x4, %esp\n" \
|
|
" popl %gs\n" \
|
|
" popl %fs\n" \
|
|
" popl %es\n" \
|
|
" popl %ds\n" \
|
|
" popa\n" \
|
|
" add $0x4, %esp\n" \
|
|
" iret\n");
|
|
|
|
static void dump(const RegisterDump& regs)
|
|
{
|
|
u16 ss;
|
|
u32 esp;
|
|
if (!current || current->process().is_ring0()) {
|
|
ss = regs.ss;
|
|
esp = regs.esp;
|
|
} else {
|
|
ss = regs.ss_if_crossRing;
|
|
esp = regs.esp_if_crossRing;
|
|
}
|
|
|
|
kprintf("exception code: %04x (isr: %04x)\n", regs.exception_code, regs.isr_number);
|
|
kprintf(" pc=%04x:%08x flags=%04x\n", (u16)regs.cs, regs.eip, (u16)regs.eflags);
|
|
kprintf(" stk=%04x:%08x\n", ss, esp);
|
|
kprintf(" ds=%04x es=%04x fs=%04x gs=%04x\n", (u16)regs.ds, (u16)regs.es, (u16)regs.fs, (u16)regs.gs);
|
|
kprintf("eax=%08x ebx=%08x ecx=%08x edx=%08x\n", regs.eax, regs.ebx, regs.ecx, regs.edx);
|
|
kprintf("ebp=%08x esp=%08x esi=%08x edi=%08x\n", regs.ebp, esp, regs.esi, regs.edi);
|
|
|
|
if (current && current->process().validate_read((void*)regs.eip, 8)) {
|
|
u8* codeptr = (u8*)regs.eip;
|
|
kprintf("code: %02x %02x %02x %02x %02x %02x %02x %02x\n",
|
|
codeptr[0],
|
|
codeptr[1],
|
|
codeptr[2],
|
|
codeptr[3],
|
|
codeptr[4],
|
|
codeptr[5],
|
|
codeptr[6],
|
|
codeptr[7]);
|
|
}
|
|
}
|
|
|
|
void handle_crash(RegisterDump& regs, const char* description, int signal)
|
|
{
|
|
if (!current) {
|
|
kprintf("%s with !current\n", description);
|
|
hang();
|
|
}
|
|
|
|
kprintf("\033[31;1mCRASH: %s. %s: %s(%u)\033[0m\n",
|
|
description,
|
|
current->process().is_ring0() ? "Kernel" : "Process",
|
|
current->process().name().characters(),
|
|
current->pid());
|
|
|
|
dump(regs);
|
|
|
|
if (current->process().is_ring0()) {
|
|
kprintf("Oh shit, we've crashed in ring 0 :(\n");
|
|
dump_backtrace();
|
|
hang();
|
|
}
|
|
|
|
cli();
|
|
current->process().crash(signal, regs.eip);
|
|
}
|
|
|
|
EH_ENTRY_NO_CODE(6, illegal_instruction);
|
|
void illegal_instruction_handler(RegisterDump regs)
|
|
{
|
|
handle_crash(regs, "Illegal instruction", SIGILL);
|
|
}
|
|
|
|
EH_ENTRY_NO_CODE(0, divide_error);
|
|
void divide_error_handler(RegisterDump regs)
|
|
{
|
|
handle_crash(regs, "Divide error", SIGFPE);
|
|
}
|
|
|
|
EH_ENTRY(13, general_protection_fault);
|
|
void general_protection_fault_handler(RegisterDump regs)
|
|
{
|
|
handle_crash(regs, "General protection fault", SIGSEGV);
|
|
}
|
|
|
|
// 7: FPU not available exception
|
|
EH_ENTRY_NO_CODE(7, fpu_exception);
|
|
void fpu_exception_handler(RegisterDump)
|
|
{
|
|
// Just clear the TS flag. We've already restored the FPU state eagerly.
|
|
// FIXME: It would be nice if we didn't have to do this at all.
|
|
asm volatile("clts");
|
|
}
|
|
|
|
// 14: Page Fault
|
|
EH_ENTRY(14, page_fault);
|
|
void page_fault_handler(RegisterDump regs)
|
|
{
|
|
ASSERT(current);
|
|
|
|
u32 fault_address;
|
|
asm("movl %%cr2, %%eax"
|
|
: "=a"(fault_address));
|
|
|
|
u32 fault_page_directory;
|
|
asm("movl %%cr3, %%eax"
|
|
: "=a"(fault_page_directory));
|
|
|
|
#ifdef PAGE_FAULT_DEBUG
|
|
dbgprintf("%s(%u): ring%u %s page fault in PD=%x, %s V%08x\n",
|
|
current->process().name().characters(),
|
|
current->pid(),
|
|
regs.cs & 3,
|
|
regs.exception_code & 1 ? "PV" : "NP",
|
|
fault_page_directory,
|
|
regs.exception_code & 2 ? "write" : "read",
|
|
fault_address);
|
|
#endif
|
|
|
|
#ifdef PAGE_FAULT_DEBUG
|
|
dump(regs);
|
|
#endif
|
|
|
|
bool faulted_in_userspace = (regs.cs & 3) == 3;
|
|
if (faulted_in_userspace && !MM.validate_user_stack(current->process(), VirtualAddress(regs.esp_if_crossRing))) {
|
|
dbgprintf("Invalid stack pointer: %p\n", regs.esp_if_crossRing);
|
|
handle_crash(regs, "Bad stack on page fault", SIGSTKFLT);
|
|
ASSERT_NOT_REACHED();
|
|
}
|
|
|
|
auto response = MM.handle_page_fault(PageFault(regs.exception_code, VirtualAddress(fault_address)));
|
|
|
|
if (response == PageFaultResponse::ShouldCrash) {
|
|
if (current->has_signal_handler(SIGSEGV)) {
|
|
current->send_urgent_signal_to_self(SIGSEGV);
|
|
return;
|
|
}
|
|
|
|
kprintf("\033[31;1m%s(%u:%u) Unrecoverable page fault, %s%s%s address %p\033[0m\n",
|
|
current->process().name().characters(),
|
|
current->pid(),
|
|
current->tid(),
|
|
regs.exception_code & PageFaultFlags::ReservedBitViolation ? "reserved bit violation / " : "",
|
|
regs.exception_code & PageFaultFlags::InstructionFetch ? "instruction fetch / " : "",
|
|
regs.exception_code & PageFaultFlags::Write ? "write to" : "read from",
|
|
fault_address);
|
|
|
|
u32 malloc_scrub_pattern = explode_byte(MALLOC_SCRUB_BYTE);
|
|
u32 free_scrub_pattern = explode_byte(FREE_SCRUB_BYTE);
|
|
if ((fault_address & 0xffff0000) == (malloc_scrub_pattern & 0xffff0000)) {
|
|
kprintf("\033[33;1mNote: Address %p looks like it may be uninitialized malloc() memory\033[0m\n", fault_address);
|
|
} else if ((fault_address & 0xffff0000) == (free_scrub_pattern & 0xffff0000)) {
|
|
kprintf("\033[33;1mNote: Address %p looks like it may be recently free()'d memory\033[0m\n", fault_address);
|
|
} else if (fault_address < 4096) {
|
|
kprintf("\033[33;1mNote: Address %p looks like a possible nullptr dereference\033[0m\n", fault_address);
|
|
}
|
|
|
|
handle_crash(regs, "Page Fault", SIGSEGV);
|
|
} else if (response == PageFaultResponse::Continue) {
|
|
#ifdef PAGE_FAULT_DEBUG
|
|
dbgprintf("Continuing after resolved page fault\n");
|
|
#endif
|
|
} else {
|
|
ASSERT_NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
#define EH(i, msg) \
|
|
static void _exception##i() \
|
|
{ \
|
|
kprintf(msg "\n"); \
|
|
u32 cr0, cr2, cr3, cr4; \
|
|
asm("movl %%cr0, %%eax" \
|
|
: "=a"(cr0)); \
|
|
asm("movl %%cr2, %%eax" \
|
|
: "=a"(cr2)); \
|
|
asm("movl %%cr3, %%eax" \
|
|
: "=a"(cr3)); \
|
|
asm("movl %%cr4, %%eax" \
|
|
: "=a"(cr4)); \
|
|
kprintf("CR0=%x CR2=%x CR3=%x CR4=%x\n", cr0, cr2, cr3, cr4); \
|
|
hang(); \
|
|
}
|
|
|
|
EH(1, "Debug exception")
|
|
EH(2, "Unknown error")
|
|
EH(3, "Breakpoint")
|
|
EH(4, "Overflow")
|
|
EH(5, "Bounds check")
|
|
EH(8, "Double fault")
|
|
EH(9, "Coprocessor segment overrun")
|
|
EH(10, "Invalid TSS")
|
|
EH(11, "Segment not present")
|
|
EH(12, "Stack exception")
|
|
EH(15, "Unknown error")
|
|
EH(16, "Coprocessor error")
|
|
|
|
static void write_raw_gdt_entry(u16 selector, u32 low, u32 high)
|
|
{
|
|
u16 i = (selector & 0xfffc) >> 3;
|
|
s_gdt[i].low = low;
|
|
s_gdt[i].high = high;
|
|
|
|
if (i > s_gdt_length)
|
|
s_gdtr.limit = (s_gdt_length + 1) * 8 - 1;
|
|
}
|
|
|
|
void write_gdt_entry(u16 selector, Descriptor& descriptor)
|
|
{
|
|
write_raw_gdt_entry(selector, descriptor.low, descriptor.high);
|
|
}
|
|
|
|
Descriptor& get_gdt_entry(u16 selector)
|
|
{
|
|
u16 i = (selector & 0xfffc) >> 3;
|
|
return *(Descriptor*)(&s_gdt[i]);
|
|
}
|
|
|
|
void flush_gdt()
|
|
{
|
|
s_gdtr.address = s_gdt;
|
|
s_gdtr.limit = (s_gdt_length * 8) - 1;
|
|
asm("lgdt %0" ::"m"(s_gdtr)
|
|
: "memory");
|
|
}
|
|
|
|
void gdt_init()
|
|
{
|
|
s_gdt_length = 5;
|
|
|
|
s_gdt_freelist = new Vector<u16>();
|
|
s_gdt_freelist->ensure_capacity(256);
|
|
for (size_t i = s_gdt_length; i < 256; ++i)
|
|
s_gdt_freelist->append(i * 8);
|
|
|
|
s_gdt_length = 256;
|
|
s_gdtr.address = s_gdt;
|
|
s_gdtr.limit = (s_gdt_length * 8) - 1;
|
|
|
|
write_raw_gdt_entry(0x0000, 0x00000000, 0x00000000);
|
|
write_raw_gdt_entry(0x0008, 0x0000ffff, 0x00cf9a00);
|
|
write_raw_gdt_entry(0x0010, 0x0000ffff, 0x00cf9200);
|
|
write_raw_gdt_entry(0x0018, 0x0000ffff, 0x00cffa00);
|
|
write_raw_gdt_entry(0x0020, 0x0000ffff, 0x00cff200);
|
|
|
|
flush_gdt();
|
|
|
|
asm volatile(
|
|
"mov %%ax, %%ds\n"
|
|
"mov %%ax, %%es\n"
|
|
"mov %%ax, %%fs\n"
|
|
"mov %%ax, %%gs\n"
|
|
"mov %%ax, %%ss\n" ::"a"(0x10)
|
|
: "memory");
|
|
|
|
// Make sure CS points to the kernel code descriptor.
|
|
asm volatile(
|
|
"ljmpl $0x8, $sanity\n"
|
|
"sanity:\n");
|
|
}
|
|
|
|
static void unimp_trap()
|
|
{
|
|
kprintf("Unhandled IRQ.");
|
|
hang();
|
|
}
|
|
|
|
void register_irq_handler(u8 irq, IRQHandler& handler)
|
|
{
|
|
ASSERT(!s_irq_handler[irq]);
|
|
s_irq_handler[irq] = &handler;
|
|
}
|
|
|
|
void unregister_irq_handler(u8 irq, IRQHandler& handler)
|
|
{
|
|
ASSERT(s_irq_handler[irq] == &handler);
|
|
s_irq_handler[irq] = nullptr;
|
|
}
|
|
|
|
void register_interrupt_handler(u8 index, void (*f)())
|
|
{
|
|
s_idt[index].low = 0x00080000 | LSW((f));
|
|
s_idt[index].high = ((u32)(f)&0xffff0000) | 0x8e00;
|
|
flush_idt();
|
|
}
|
|
|
|
void register_user_callable_interrupt_handler(u8 index, void (*f)())
|
|
{
|
|
s_idt[index].low = 0x00080000 | LSW((f));
|
|
s_idt[index].high = ((u32)(f)&0xffff0000) | 0xef00;
|
|
flush_idt();
|
|
}
|
|
|
|
void flush_idt()
|
|
{
|
|
asm("lidt %0" ::"m"(s_idtr));
|
|
}
|
|
|
|
GENERATE_IRQ_ASM_ENTRY(0, 0x50)
|
|
GENERATE_IRQ_ASM_ENTRY(1, 0x51)
|
|
GENERATE_IRQ_ASM_ENTRY(2, 0x52)
|
|
GENERATE_IRQ_ASM_ENTRY(3, 0x53)
|
|
GENERATE_IRQ_ASM_ENTRY(4, 0x54)
|
|
GENERATE_IRQ_ASM_ENTRY(5, 0x55)
|
|
GENERATE_IRQ_ASM_ENTRY(6, 0x56)
|
|
GENERATE_IRQ_ASM_ENTRY(7, 0x57)
|
|
GENERATE_IRQ_ASM_ENTRY(8, 0x58)
|
|
GENERATE_IRQ_ASM_ENTRY(9, 0x59)
|
|
GENERATE_IRQ_ASM_ENTRY(10, 0x5a)
|
|
GENERATE_IRQ_ASM_ENTRY(11, 0x5b)
|
|
GENERATE_IRQ_ASM_ENTRY(12, 0x5c)
|
|
GENERATE_IRQ_ASM_ENTRY(13, 0x5d)
|
|
GENERATE_IRQ_ASM_ENTRY(14, 0x5e)
|
|
GENERATE_IRQ_ASM_ENTRY(15, 0x5f)
|
|
|
|
void idt_init()
|
|
{
|
|
s_idtr.address = s_idt;
|
|
s_idtr.limit = 0x100 * 8 - 1;
|
|
|
|
for (u8 i = 0xff; i > 0x10; --i)
|
|
register_interrupt_handler(i, unimp_trap);
|
|
|
|
register_interrupt_handler(0x00, divide_error_asm_entry);
|
|
register_interrupt_handler(0x01, _exception1);
|
|
register_interrupt_handler(0x02, _exception2);
|
|
register_interrupt_handler(0x03, _exception3);
|
|
register_interrupt_handler(0x04, _exception4);
|
|
register_interrupt_handler(0x05, _exception5);
|
|
register_interrupt_handler(0x06, illegal_instruction_asm_entry);
|
|
register_interrupt_handler(0x07, fpu_exception_asm_entry);
|
|
register_interrupt_handler(0x08, _exception8);
|
|
register_interrupt_handler(0x09, _exception9);
|
|
register_interrupt_handler(0x0a, _exception10);
|
|
register_interrupt_handler(0x0b, _exception11);
|
|
register_interrupt_handler(0x0c, _exception12);
|
|
register_interrupt_handler(0x0d, general_protection_fault_asm_entry);
|
|
register_interrupt_handler(0x0e, page_fault_asm_entry);
|
|
register_interrupt_handler(0x0f, _exception15);
|
|
register_interrupt_handler(0x10, _exception16);
|
|
|
|
register_interrupt_handler(0x50, irq_0_asm_entry);
|
|
register_interrupt_handler(0x51, irq_1_asm_entry);
|
|
register_interrupt_handler(0x52, irq_2_asm_entry);
|
|
register_interrupt_handler(0x53, irq_3_asm_entry);
|
|
register_interrupt_handler(0x54, irq_4_asm_entry);
|
|
register_interrupt_handler(0x55, irq_5_asm_entry);
|
|
register_interrupt_handler(0x56, irq_6_asm_entry);
|
|
register_interrupt_handler(0x57, irq_7_asm_entry);
|
|
register_interrupt_handler(0x58, irq_8_asm_entry);
|
|
register_interrupt_handler(0x59, irq_9_asm_entry);
|
|
register_interrupt_handler(0x5a, irq_10_asm_entry);
|
|
register_interrupt_handler(0x5b, irq_11_asm_entry);
|
|
register_interrupt_handler(0x5c, irq_12_asm_entry);
|
|
register_interrupt_handler(0x5d, irq_13_asm_entry);
|
|
register_interrupt_handler(0x5e, irq_14_asm_entry);
|
|
register_interrupt_handler(0x5f, irq_15_asm_entry);
|
|
|
|
for (u8 i = 0; i < 16; ++i) {
|
|
s_irq_handler[i] = nullptr;
|
|
}
|
|
|
|
flush_idt();
|
|
}
|
|
|
|
void load_task_register(u16 selector)
|
|
{
|
|
asm("ltr %0" ::"r"(selector));
|
|
}
|
|
|
|
void handle_irq(RegisterDump regs)
|
|
{
|
|
ASSERT(regs.isr_number >= 0x50 && regs.isr_number <= 0x5f);
|
|
u8 irq = (u8)(regs.isr_number - 0x50);
|
|
if (s_irq_handler[irq])
|
|
s_irq_handler[irq]->handle_irq();
|
|
PIC::eoi(irq);
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
void __assertion_failed(const char* msg, const char* file, unsigned line, const char* func)
|
|
{
|
|
asm volatile("cli");
|
|
kprintf("ASSERTION FAILED: %s\n%s:%u in %s\n", msg, file, line, func);
|
|
dump_backtrace();
|
|
asm volatile("hlt");
|
|
for (;;)
|
|
;
|
|
}
|
|
#endif
|
|
|
|
void sse_init()
|
|
{
|
|
asm volatile(
|
|
"mov %cr0, %eax\n"
|
|
"andl $0xfffffffb, %eax\n"
|
|
"orl $0x2, %eax\n"
|
|
"mov %eax, %cr0\n"
|
|
"mov %cr4, %eax\n"
|
|
"orl $0x600, %eax\n"
|
|
"mov %eax, %cr4\n");
|
|
}
|
|
|
|
bool g_cpu_supports_nx;
|
|
bool g_cpu_supports_pae;
|
|
bool g_cpu_supports_pge;
|
|
bool g_cpu_supports_rdrand;
|
|
bool g_cpu_supports_smep;
|
|
bool g_cpu_supports_sse;
|
|
bool g_cpu_supports_tsc;
|
|
bool g_cpu_supports_umip;
|
|
|
|
void detect_cpu_features()
|
|
{
|
|
CPUID processor_info(0x1);
|
|
g_cpu_supports_pae = (processor_info.edx() & (1 << 6));
|
|
g_cpu_supports_pge = (processor_info.edx() & (1 << 13));
|
|
g_cpu_supports_sse = (processor_info.edx() & (1 << 25));
|
|
g_cpu_supports_tsc = (processor_info.edx() & (1 << 4));
|
|
g_cpu_supports_rdrand = (processor_info.ecx() & (1 << 30));
|
|
|
|
CPUID extended_processor_info(0x80000001);
|
|
g_cpu_supports_nx = (extended_processor_info.edx() & (1 << 20));
|
|
|
|
CPUID extended_features(0x7);
|
|
g_cpu_supports_smep = (extended_features.ebx() & (1 << 7));
|
|
g_cpu_supports_umip = (extended_features.ecx() & (1 << 2));
|
|
}
|