Khi phát triển bootloader/kernel, việc hiểu kiến trúc cơ bản là rất quan trọng để tối ưu hóa hiệu suất và khả năng tương thích giữa phần mềm và phần cứng.
Một công cụ quan trọng nhưng đôi khi bị bỏ qua dành cho các kỹ sư để truy vấn và truy xuất thông tin hệ thống là lệnh CPUID.
Lệnh CPUID là gì?
Lệnh CPUID là lệnh cấp thấp, nằm trong trung tâm của mọi bộ xử lý x86 và x86-64 hiện đại, cho phép phần mềm truy vấn CPU để biết thông tin về bộ xử lý và các tính năng được hỗ trợ của nó.
Bằng cách gọi hướng dẫn này, bạn có thể thu thập thông tin như kiểu bộ xử lý, dòng, kích thước bộ đệm trong và các tính năng được hỗ trợ như SIMD hoặc ảo hóa phần cứng. Điều này có thể giúp bạn tối ưu hóa hiệu suất và tự động bật hoặc tắt các tính năng được hỗ trợ.
Đối với các nhà phát triển bộ nạp khởi động hoặc hạt nhân, việc hiểu những tính năng mà bộ xử lý hỗ trợ—chẳng hạn như ảo hóa phần cứng, kích thước bộ đệm hoặc hướng dẫn SIMD—có thể đảm bảo rằng hệ thống chạy hiệu quả và mã bạn viết tương thích trên các CPU khác nhau. Bằng cách sử dụng lệnh CPUID, bạn có thể tự động điều chỉnh hành vi của hạt nhân dựa trên bộ xử lý cụ thể mà hạt nhân đang chạy.
Trong bài viết này, bạn sẽ tìm hiểu cách kiểm tra xem lệnh CPUID có sẵn cho hệ thống của bạn hay không, cách thức hoạt động và những thông tin bạn có thể nhận được khi sử dụng nó.
Điều kiện tiên quyết
-
Một số kiến thức về hợp ngữ (ví dụ này tôi sử dụng FASM)
-
Một số kiến thức về hệ điều hành/kernel
-
Truy cập vào các công cụ gỡ lỗi cấp thấp (ví dụ:GDB) hoặc trình mô phỏng phần cứng như QEMU để kiểm tra bộ nạp khởi động/kernel của bạn trên nhiều nền tảng khác nhau.
Bước 1:Kiểm tra tính khả dụng của CPUID
Trước khi thực hiện lệnh CPUID, điều quan trọng là phải xác định xem bộ xử lý có hỗ trợ nó hay không, vì không phải tất cả CPU đều được đảm bảo có chức năng này. Đoạn mã sau kiểm tra tính khả dụng của lệnh CPUID bằng cách sửa đổi và kiểm tra bit ID (bit 21) trong thanh ghi EFLAGS.
Đây là hình ảnh từ wiki.osdev.org hiển thị từng bit của thanh ghi EFLAGS:

Nếu bộ xử lý cho phép bật bit này thì CPUID sẽ được hỗ trợ; nếu không thì không. Đây là cách hoạt động của quá trình phát hiện:
(hầu hết mọi người nghĩ rằng ở chế độ Thực, 32 thanh ghi không thể truy cập được. Điều đó không đúng. Tất cả các thanh ghi 32 bit đều có thể sử dụng được)
cpuid_check:
pusha ; save state
pushfd ; Save EFLAGS
pushfd ; Store EFLAGS
xor dword [esp],0x00200000 ; Invert the ID bit in stored EFLAGS
popfd ; Load stored EFLAGS (with ID bit inverted)
pushfd ; Store EFLAGS again (ID bit may or may not be inverted)
pop eax ; eax = modified EFLAGS (ID bit may or may not be inverted)
xor eax,[esp] ; eax = whichever bits were changed
popfd ; Restore original EFLAGS
and eax,0x00200000 ; eax = zero if ID bit can't be changed, else non-zero
cmp eax,0x00
je .cpuid_instruction_not_is_available
.cpuid_instruction_is_available:
;handle CPUID exists
.cpuid_instruction_not_is_available:
;handle CPUID isn't supported
.cpuid_check_end:
popa ; restore state
ret
pusha :Lưu tất cả các thanh ghi mục đích chung để đảm bảo trạng thái ban đầu có thể được khôi phục vào cuối.
pushfd :Lưu thanh ghi EFLAGS hiện tại.
pushfd :Lưu trữ một bản sao của EFLAGS.
xor dword [esp], 0x00200000 :Mã lật bit ID (21) của EFLAGS bằng toán tử XOR.
popfd :Khôi phục EFLAGS đã sửa đổi với bit ID bị đảo ngược.
pushfd :Đẩy EFLAGS đã sửa đổi trở lại ngăn xếp.
pop eax :Đặt EFLAGS đã sửa đổi (bit ID có thể đảo ngược hoặc không) vào thanh ghi EAX.
xor eax, [esp] :Sau thao tác XOR, EAX sẽ chứa các bit đã được thay đổi.
popfd :Khôi phục EFLAGS ban đầu.
and eax, 0x00200000 :and hoạt động cô lập bit thứ 21 (bit ID) bằng cách che giấu tất cả các bit khác. Sau thao tác này, thanh ghi EAX sẽ chứa 0x00200000 (nếu 21 bit được thay đổi nghĩa là CPUID được hỗ trợ) hoặc 0×00 (21 bit không thay đổi, CPUID không được hỗ trợ).
cmp eax, 0x00 :Lệnh CMP kiểm tra kết quả của thao tác trước đó. Nếu EAX bằng 0×00, điều đó có nghĩa là bit ID không thể sửa đổi được và bộ xử lý không hỗ trợ lệnh CPUID. Nếu nó không bằng 0, điều đó có nghĩa là bit ID đã bị lật và bộ xử lý của bạn hỗ trợ lệnh CPUID.
Nhận các tính năng của CPU
Lệnh CPUID sẽ trả về các thông tin khác nhau với các giá trị khác nhau trong thanh ghi EAX.
mov eax, 0x1
cpuid
Với EAX được đặt thành 1, CPUID sẽ trả về một trường bit trong EDX, chứa các giá trị sau. Các thương hiệu khác nhau có thể mang lại ý nghĩa khác nhau cho những điều này (nguồn https://wiki.osdev.org/CPUID)
enum {
CPUID_FEAT_ECX_SSE3 = 1 << 0,
CPUID_FEAT_ECX_PCLMUL = 1 << 1,
CPUID_FEAT_ECX_DTES64 = 1 << 2,
CPUID_FEAT_ECX_MONITOR = 1 << 3,
CPUID_FEAT_ECX_DS_CPL = 1 << 4,
CPUID_FEAT_ECX_VMX = 1 << 5,
CPUID_FEAT_ECX_SMX = 1 << 6,
CPUID_FEAT_ECX_EST = 1 << 7,
CPUID_FEAT_ECX_TM2 = 1 << 8,
CPUID_FEAT_ECX_SSSE3 = 1 << 9,
CPUID_FEAT_ECX_CID = 1 << 10,
CPUID_FEAT_ECX_SDBG = 1 << 11,
CPUID_FEAT_ECX_FMA = 1 << 12,
CPUID_FEAT_ECX_CX16 = 1 << 13,
CPUID_FEAT_ECX_XTPR = 1 << 14,
CPUID_FEAT_ECX_PDCM = 1 << 15,
CPUID_FEAT_ECX_PCID = 1 << 17,
CPUID_FEAT_ECX_DCA = 1 << 18,
CPUID_FEAT_ECX_SSE4_1 = 1 << 19,
CPUID_FEAT_ECX_SSE4_2 = 1 << 20,
CPUID_FEAT_ECX_X2APIC = 1 << 21,
CPUID_FEAT_ECX_MOVBE = 1 << 22,
CPUID_FEAT_ECX_POPCNT = 1 << 23,
CPUID_FEAT_ECX_TSC = 1 << 24,
CPUID_FEAT_ECX_AES = 1 << 25,
CPUID_FEAT_ECX_XSAVE = 1 << 26,
CPUID_FEAT_ECX_OSXSAVE = 1 << 27,
CPUID_FEAT_ECX_AVX = 1 << 28,
CPUID_FEAT_ECX_F16C = 1 << 29,
CPUID_FEAT_ECX_RDRAND = 1 << 30,
CPUID_FEAT_ECX_HYPERVISOR = 1 << 31,
CPUID_FEAT_EDX_FPU = 1 << 0,
CPUID_FEAT_EDX_VME = 1 << 1,
CPUID_FEAT_EDX_DE = 1 << 2,
CPUID_FEAT_EDX_PSE = 1 << 3,
CPUID_FEAT_EDX_TSC = 1 << 4,
CPUID_FEAT_EDX_MSR = 1 << 5,
CPUID_FEAT_EDX_PAE = 1 << 6,
CPUID_FEAT_EDX_MCE = 1 << 7,
CPUID_FEAT_EDX_CX8 = 1 << 8,
CPUID_FEAT_EDX_APIC = 1 << 9,
CPUID_FEAT_EDX_SEP = 1 << 11,
CPUID_FEAT_EDX_MTRR = 1 << 12,
CPUID_FEAT_EDX_PGE = 1 << 13,
CPUID_FEAT_EDX_MCA = 1 << 14,
CPUID_FEAT_EDX_CMOV = 1 << 15,
CPUID_FEAT_EDX_PAT = 1 << 16,
CPUID_FEAT_EDX_PSE36 = 1 << 17,
CPUID_FEAT_EDX_PSN = 1 << 18,
CPUID_FEAT_EDX_CLFLUSH = 1 << 19,
CPUID_FEAT_EDX_DS = 1 << 21,
CPUID_FEAT_EDX_ACPI = 1 << 22,
CPUID_FEAT_EDX_MMX = 1 << 23,
CPUID_FEAT_EDX_FXSR = 1 << 24,
CPUID_FEAT_EDX_SSE = 1 << 25,
CPUID_FEAT_EDX_SSE2 = 1 << 26,
CPUID_FEAT_EDX_SS = 1 << 27,
CPUID_FEAT_EDX_HTT = 1 << 28,
CPUID_FEAT_EDX_TM = 1 << 29,
CPUID_FEAT_EDX_IA64 = 1 << 30,
CPUID_FEAT_EDX_PBE = 1 << 31
};
Giải thích ngắn gọn về các tính năng của CPU ở trên:
-
PCLMUL, AES:Bộ hướng dẫn mật mã để mã hóa và giải mã nhanh. -
VMX, SMX:Hỗ trợ ảo hóa để chạy máy ảo. -
SSE3, SSSE3, SSE4.1, SSE4.2, AVX:Bộ hướng dẫn SIMD để xử lý đa phương tiện, toán học và vectơ nhanh hơn. -
FMA:Hợp nhất Nhân-Cộng, cải thiện hiệu suất trong các phép tính dấu phẩy động. -
RDRAND:Trình tạo số ngẫu nhiên. -
X2APIC:Xử lý ngắt nâng cao trong hệ thống đa bộ xử lý. -
PCID:Tối ưu hóa việc quản lý bộ nhớ trong quá trình chuyển đổi ngữ cảnh. -
FPU:Đơn vị dấu phẩy động phần cứng giúp thực hiện các phép toán nhanh hơn. -
PAE:Tiện ích mở rộng địa chỉ vật lý, cho phép đánh địa chỉ hơn 4 GB bộ nhớ. -
HTT:Cho phép một lõi CPU xử lý nhiều luồng. -
PAT, PGE:Tính năng quản lý bộ nhớ để kiểm soát bộ nhớ đệm và ánh xạ trang. -
MMX, SSE, SSE2:Bộ hướng dẫn SIMD cũ hơn để xử lý đa phương tiện.
Nhận chuỗi nhà cung cấp CPU
Nếu bạn muốn lấy chuỗi nhà cung cấp CPU, EAX phải được đặt thành 0×0 trước khi gọi lệnh CPUID.
mov eax, 0x0
cpuid
Chuỗi nhà cung cấp là mã định danh duy nhất mà các nhà cung cấp CPU như AMD và Intel sử dụng. Ví dụ như:Chính hãngIntel (đối với bộ xử lý Intel) hoặc AuthenticAMD (đối với bộ xử lý AMD). Về cơ bản nó chỉ định nhà sản xuất CPU.
Chuỗi nhà cung cấp cho phép kernel xác định nhà sản xuất CPU, điều này rất hữu ích vì các nhà sản xuất khác nhau triển khai các tính năng nhất định một cách khác nhau. Ngoài ra, phần mềm hoặc trình điều khiển có thể tương tác khác nhau tùy theo nhà sản xuất CPU để đảm bảo khả năng tương thích.
Khi được sử dụng như thế này, chuỗi id nhà cung cấp sẽ được trả về trong các thanh ghi EBX, EDX, ECX. Bạn có thể ghi chúng vào bộ đệm và lấy chuỗi 12 ký tự đầy đủ.
Mã ví dụ:
Bước 1:Bộ đệm
Tạo bộ đệm có thể chứa 12 byte:
buffer: db 12 dup(0), 0xA, 0xD, 0
Bước 2:In bộ đệm
Chúng ta bắt đầu bằng cách tạo một hàm in chuỗi.
Mã hợp ngữ này đọc từng ký tự chuỗi và in nó ra màn hình bằng cách sử dụng ngắt BIOS 0x10. print hàm lặp qua chuỗi và sử dụng lodsb hướng dẫn tải từng ký tự trong al đăng ký.
Sau đó là print_char Hàm sử dụng ngắt 0×10 để in nó ra màn hình. Khi mã đến cuối chuỗi (bộ kết thúc null), vòng lặp kết thúc.
print_string:
call print
ret
print:
.loop:
lodsb ;read character to al and then increment
cmp al ,0 ;check if we reached the end
je .done ;we reached null terminator, finish
call print_char ;print character
jmp .loop ;jump back into the loop
.done:
ret
print_char:
mov ah, 0eh
int 0x10
ret
Bước 3:Điền vào bộ đệm và in nó
Ở đây, sau khi lưu trạng thái hiện tại bằng pusha hướng dẫn và gọi cpuid với 0×0 được chuyển vào thanh ghi EAX, chúng ta có thể lưu trữ nội dung của ebx , edx , ecx vào bộ đệm. Sau đó chúng tôi gọi print_string để in nó.
get_cpu_vendor:
pusha
mov eax, 0x0
cpuid
mov [buffer], ebx
mov [buffer + 4], edx
mov [buffer + 8], ecx
mov si, buffer
call print_string
popa
ret
Một video từ kênh YouTube của tôi nơi tôi triển khai và giải thích chi tiết đoạn mã trên
Bạn có thể tìm thêm thông tin về những thông tin mà lệnh CPUID có thể cung cấp cho bạn theo giá trị được truyền trong thanh ghi EAX tại đây:https://gitlab.com/x86-cpuid.org/x86-cpuid-db
Phần kết
Bằng cách hiểu và sử dụng lệnh CPUID, bạn có thể làm cho bộ nạp khởi động/hạt nhân của mình thích ứng hơn với nhiều loại bộ xử lý. Biết cách phát hiện tính khả dụng của lệnh và truy xuất thông tin quan trọng của hệ thống—chẳng hạn như tính năng CPU, kích thước bộ nhớ đệm và các công nghệ được hỗ trợ—có thể nâng cao đáng kể hiệu suất và khả năng tương thích.
Sau khi đọc bài viết này, bạn sẽ có các công cụ và kiến thức để bắt đầu khám phá hướng dẫn CPUID và cách bạn có thể sử dụng nó trong dự án của riêng mình!
Chúc bạn viết mã vui vẻ!
Học cách viết mã miễn phí. Chương trình giảng dạy mã nguồn mở của freeCodeCamp đã giúp hơn 40.000 người có được việc làm với tư cách là nhà phát triển. Bắt đầu