[{"content":"Phát Triển Hệ Điều Hành Từ Con Số 0 Chương 0 - Lời Nói Đầu Series về việc tự tay xây dựng một hệ điều hành hoạt động được - từ byte đầu tiên của boot sector cho tới một kernel biết chạy nhiều tiến trình cùng lúc.\nMáy thật: bật Legacy/CSM boot trên một chiếc ThinkPad để nó chịu boot ảnh đĩa của chính mình từ USB.\nTải video\nMáy thật: bật Legacy/CSM boot trên một chiếc ThinkPad để nó chịu boot ảnh đĩa của chính mình từ USB. Chào mừng bạn Đây là một series các bài viết và hướng dẫn về máy tính và hệ điều hành. Mục tiêu không phải là ghép đại một OS đồ chơi rồi coi như xong - mà là hiểu tại sao mọi thứ vận hành như vậy. Từ khoảnh khắc bạn bấm nút nguồn và BIOS trao quyền điều khiển, cho tới khi kernel của chính bạn in dòng chữ đầu tiên lên màn hình.\nTrên đường đi, chúng ta sẽ chạm vào gần như mọi tầng của một chiếc máy: kiến trúc CPU, quá trình boot, quản lý bộ nhớ, ngắt, trình điều khiển thiết bị, hệ thống tập tin và đa nhiệm. Đây là loại kiến thức bạn hiếm khi cần khi viết ứng dụng thông thường - nhưng một khi có nó, bạn sẽ không bao giờ nhìn máy tính theo cách cũ nữa.\nMình viết series này với một nguyên tắc: giải thích mọi thứ, không giấu phép màu. Mỗi khi có một dòng kiểu \u0026ldquo;cứ gõ vậy đi rồi nó chạy\u0026rdquo;, mình sẽ cố hết sức cho bạn biết dòng đó thực sự làm gì.\nCuối series bạn sẽ xây được gì? Để bạn hình dung con đường phía trước, đây là lộ trình tổng quát:\nBoot sector 512 byte - đoạn code assembly đầu tiên mà CPU thực thi, chạy ở real mode 16-bit. Bootloader nhiều tầng - nạp phần kernel lớn hơn từ đĩa vào bộ nhớ. Chuyển sang Protected Mode (32-bit) - thiết lập GDT và bật các cơ chế bảo vệ của CPU. Kernel viết bằng C - in ký tự ra màn hình qua VGA text buffer. Xử lý ngắt - IDT, ngoại lệ CPU, ngắt phần cứng (IRQ), bàn phím và timer. Quản lý bộ nhớ - physical memory manager, paging, bộ nhớ ảo, và cuối cùng là malloc/free của riêng ta. Trình điều khiển thiết bị - bàn phím, ổ đĩa, timer. Hệ thống tập tin - đọc file từ một định dạng đĩa đơn giản như FAT. Đa nhiệm - chuyển ngữ cảnh, bộ lập lịch, và chạy nhiều tác vụ song song. (Nâng cao, tùy chọn) - Long Mode 64-bit, không gian người dùng, và system call. Chưa cần hiểu hết ngay đâu - đây chỉ là bản đồ. Chúng ta sẽ đi từng bước một.\nBạn cần biết gì trước? Series này dùng C và hợp ngữ x86 (x86 assembly). Bạn nên nắm vững cả hai trước khi bắt đầu. Chương này có phần ôn nhanh những điểm quan trọng nhất.\nNếu bạn chưa từng lập trình Chào mừng đến với lập trình! Nhưng nói thật: phát triển hệ điều hành là chỗ cực tệ để học code lần đầu. Ở đây không có lưới an toàn - không có thông báo lỗi thân thiện, không có print để debug dễ dàng, và một lỗi nhỏ có thể làm cả máy đơ cứng mà không nói một lời.\nLời khuyên: hãy bắt đầu với thứ nhẹ nhàng hơn như Python để xây dựng trực giác lập trình, rồi chuyển sang C. Khi con trỏ, quản lý bộ nhớ thủ công, và cách máy tính thực sự lưu trữ dữ liệu đã trở nên tự nhiên với bạn, hãy quay lại đây. Nó sẽ đáng giá hơn nhiều.\nMôi trường phát triển: Ubuntu Linux Toàn bộ series này được phát triển và kiểm thử trên Ubuntu Linux. Linux là môi trường cực kỳ tự nhiên cho việc dev OS: công cụ mã nguồn mở đầy đủ, cross-compiler có sẵn, và bạn kiểm soát được mọi thứ.\nCài đặt bộ công cụ:\n1sudo apt update 2sudo apt install build-essential nasm qemu-system-x86 gdb build-essential - GCC, make, và các công cụ biên dịch cốt lõi. nasm - trình hợp dịch cho code x86 assembly của chúng ta. qemu-system-x86 - máy ảo để chạy và thử nghiệm OS, thay vì reboot máy thật mỗi lần. Nhanh, và không thể làm hỏng máy. gdb - trình gỡ lỗi, sẽ được cắm thẳng vào QEMU để chạy từng bước qua kernel. Chúng ta sẽ tự viết bootloader bằng assembly, nên không cần GRUB hay bất kỳ công cụ đóng gói ảnh đĩa nào. Sản phẩm cuối là một ảnh đĩa thô (flat binary) chạy thẳng trong QEMU, hoặc ghi ra USB để boot trên máy thật (xem phần cuối).\nTại sao phải dùng cross-compiler Đây là điều mà nguồn cảm hứng gốc của series bỏ qua, và nó khiến gần như ai cũng vấp. Nó xứng đáng có phần giải thích riêng.\nGCC mặc định trên Ubuntu được cấu hình để tạo ra chương trình chạy trên Linux. Nó âm thầm cài vào những giả định về hệ điều hành chủ - thư viện chuẩn, cách chương trình khởi động, định dạng thực thi, v.v. Nhưng kernel của bạn chính là hệ điều hành. Nó không chạy bên trên Linux, nên tất cả những giả định đó biến thành các lỗi khó chịu và khó hiểu.\nGiải pháp là xây dựng một cross-compiler riêng, ví dụ i686-elf-gcc (cho 32-bit) hoặc x86_64-elf-gcc (cho 64-bit). Đây là bản GCC được cấu hình để xuất ra code \u0026ldquo;freestanding\u0026rdquo; - không giả định có hệ điều hành nào bên dưới. Chúng ta sẽ hướng dẫn cách build nó ở một chương riêng - hãy xem đây là bước bắt buộc, không phải tùy chọn.\nTổng quan về C trong \u0026ldquo;cõi kernel\u0026rdquo; Phần này giả định bạn đã biết C. Nó chỉ chỉ ra những chỗ mà C hoạt động khác so với khi viết ứng dụng.\nC 16-bit và 32-bit Ngay khi bật nguồn, CPU chạy ở real mode 16-bit - chế độ mà các trình biên dịch 32-bit hiện đại không hỗ trợ. Đây là điều quan trọng đầu tiên: nếu muốn OS real-mode 16-bit, bạn cần trình biên dịch C 16-bit; nếu muốn OS 32-bit, bạn cần trình biên dịch 32-bit. Code C 16-bit và 32-bit không tương thích với nhau.\nTrong series này, chúng ta xây dựng một hệ điều hành 32-bit (rồi sau đó mở rộng lên 64-bit), nên sẽ dùng cross-compiler tương ứng.\nC và các định dạng tập tin thực thi Một điểm vướng với C là nó không xuất ra được flat binary - loại chương trình mà điểm bắt đầu nằm ngay ở byte đầu tiên, không có cấu trúc nội bộ nào cả, chỉ là một chuỗi các số 0 và 1.\nTại sao ta lại cần nó? Vì khi máy boot, BIOS ROM nắm quyền, và khi cần khởi động một OS thì nó hoàn toàn không biết mọi thứ đang ở định dạng gì. Nó chỉ đơn giản nạp boot sector vào bộ nhớ rồi nhảy tới byte đầu tiên. Nó coi bootloader như một flat binary thuần túy - không hơn.\n(Có một chi tiết cụ thể khá hay ở đây: BIOS chỉ chấp nhận một boot sector nếu hai byte cuối của nó là chữ ký 0x55 0xAA. Thiếu nó là máy từ chối boot ngay. Đây là một trong những \u0026ldquo;nghi thức\u0026rdquo; nhỏ đầu tiên bạn sẽ học.)\nVì vậy, phần đầu của bootloader (Tầng 1) bắt buộc phải là assembly. Mọi trình biên dịch C đều xuất ra tập tin có cấu trúc nội bộ - object file, thư viện, hoặc file thực thi như ELF. Chỉ có một ngôn ngữ hỗ trợ trực tiếp flat binary thô: hợp ngữ.\nTrên Linux, định dạng nội bộ mà GCC tạo ra là ELF (Executable and Linkable Format), và đó là thứ ta dùng cho kernel. Ta sẽ dùng một linker script để kiểm soát chính xác cách từng phần của chương trình được bố trí trong bộ nhớ, và objcopy để trích ra flat binary khi cần.\nGọi kernel viết bằng C Khi bootloader đã sẵn sàng, nó nạp và chạy kernel C bằng cách gọi vào điểm vào (entry point) của kernel. Vì chương trình C tuân theo một định dạng nội bộ nhất định (ELF), bootloader phải biết cách phân tích tập tin đó và tìm điểm vào để gọi. Ta sẽ nói chính xác cách làm ở phần sau. Đây là điều cho phép ta viết kernel và các thư viện bằng C, thay vì phải hợp dịch mọi thứ bằng tay.\nChuỗi khởi động: mỗi tầng nạp tầng tiếp theo rồi trao quyền điều khiển, cho tới khi kernel của bạn tiếp quản. Chú ý rằng Tầng 1 phải vừa trong một sector 512 byte và kết thúc bằng chữ ký ma thuật 0x55AA.\nCon trỏ và Không gian địa chỉ vật lý (PAS) Vì sao con trỏ quan trọng đến vậy ở đây Trong phần mềm hệ thống, con trỏ có mặt ở khắp nơi. Con trỏ đơn giản là một biến chứa địa chỉ của một thứ gì đó:\n1char* pointer; Con trỏ này chưa được gán gì cả - đây là một wild pointer (con trỏ hoang). Nó có thể trỏ tới bất cứ đâu: một biến khác, địa chỉ 0, chính đoạn code của bạn, hay một thanh ghi phần cứng. C không khởi tạo giá trị cho bạn.\nKhông gian địa chỉ vật lý (PAS) Không gian địa chỉ vật lý định nghĩa tất cả các địa chỉ bạn có thể dùng. Những địa chỉ này có thể trỏ tới bất cứ thứ gì trong PAS: bộ nhớ vật lý (RAM), thiết bị phần cứng, hoặc thậm chí là hư không. Đây là điểm khác biệt lớn so với lập trình ứng dụng trên một OS ở protected mode, nơi mọi \u0026ldquo;địa chỉ\u0026rdquo; đều là bộ nhớ.\nĐây là ví dụ kinh điển. Trong một ứng dụng, đoạn này sập với lỗi segmentation fault:\n1char* pointer = 0; 2*pointer = 0; Chạy đúng đoạn code đó trong kernel tương lai của chúng ta\u0026hellip; và chẳng có gì sập cả. Thay vào đó, nó ghi đè lên byte đầu tiên của Interrupt Vector Table.\nTừ đó rút ra vài điều quan trọng:\nHệ thống sẽ không sập khi ghi qua con trỏ null. Con trỏ có thể trỏ tới bất cứ thứ gì trong PAS - có thể là bộ nhớ, có thể không. Đọc từ một địa chỉ không tồn tại cho bạn giá trị rác (bất cứ thứ gì đang có trên bus dữ liệu). Ghi vào đó thì chẳng làm gì cả. Các thiết bị ROM như BIOS cũng được ánh xạ vào cùng PAS - bạn đọc được chúng, nhưng ghi vào ROM cũng vô ích như ghi vào hư không.\nBản đồ sơ lược của không gian địa chỉ vật lý ở real mode. Cùng một phép tính con trỏ có thể đưa bạn tới RAM, tới một thiết bị phần cứng, hoặc tới ROM chỉ-đọc - riêng địa chỉ không nói cho bạn biết đó là loại nào.\nNói cách khác, đừng nghĩ con trỏ là \u0026ldquo;một biến trỏ tới một ô nhớ\u0026rdquo;, mà hãy nghĩ nó là \u0026ldquo;một biến trỏ tới một địa chỉ trong PAS\u0026rdquo; - nơi đó có thể là RAM, và cũng có thể là thứ hoàn toàn khác.\nCấp phát bộ nhớ Ở cõi ứng dụng, bạn gọi malloc()/free() để xin bộ nhớ từ heap. Ở cõi hệ thống, mọi thứ khởi đầu rất khác. Ban đầu, để \u0026ldquo;cấp phát\u0026rdquo; bộ nhớ, bạn chỉ làm thế này:\n1char* pointer = (char*)0x5000; Vậy thôi. Vì ta kiểm soát mọi thứ, ta chỉ cần trỏ tới một địa chỉ nào đó trong PAS (tốt nhất là RAM) và tuyên bố \u0026ldquo;đây là buffer mới của mình\u0026rdquo;.\nĐiểm mấu chốt: ban đầu không hề có cấp phát bộ nhớ động. malloc/free là các dịch vụ hệ thống - chúng cần một OS đang chạy để hỗ trợ. Nhưng khoan - chẳng phải chúng ta đang xây OS sao? Đúng vậy. Đó chính là vấn đề. Chúng ta sẽ phải tự viết các dịch vụ quản lý bộ nhớ trước khi có được malloc/free thật sự. Cho tới lúc đó, cách duy nhất để \u0026ldquo;cấp phát\u0026rdquo; là chiếm lấy một vùng địa chỉ chưa ai dùng.\nAssembly nội tuyến (Inline assembly) Có những việc C không tự làm được - giao tiếp với phần cứng, gọi các dịch vụ cấp thấp của CPU. Với những việc đó, ta phải dùng assembly.\nVới GCC trên Ubuntu, từ khóa là __asm__, và cú pháp theo phong cách AT\u0026amp;T:\n1__asm__ volatile (\u0026#34;cli\u0026#34;); // tắt ngắt Bạn cũng có thể viết cả khối lệnh:\n1__asm__ volatile ( 2 \u0026#34;cli\\n\\t\u0026#34; 3 \u0026#34;hlt\u0026#34; 4); Cú pháp extended inline assembly của GCC còn cho phép truyền biến C vào và ra. Ta sẽ dựa vào nó liên tục khi viết các hàm đọc/ghi cổng I/O để nói chuyện với phần cứng - ví dụ, đọc một byte từ cổng I/O:\n1static inline uint8_t inb(uint16_t port) { 2 uint8_t value; 3 __asm__ volatile (\u0026#34;inb %1, %0\u0026#34; : \u0026#34;=a\u0026#34;(value) : \u0026#34;Nd\u0026#34;(port)); 4 return value; 5} Chi tiết cú pháp đó sẽ có chương riêng; giờ chỉ cần biết đây là cách C và phần cứng học cách nói chuyện với nhau.\nThư viện chuẩn và Runtime (RTL) Bạn có thể dùng thư viện ngoài - nhưng chỉ với những routine không phụ thuộc vào dịch vụ hệ thống. Những thứ như printf(), scanf(), hay các hàm cấp phát bộ nhớ rốt cuộc đều cần một OS đang chạy. Trên thực tế, khoảng 90% thư viện chuẩn phải được viết lại cho OS của bạn, nên tốt nhất là tự viết ngay từ đầu.\nRuntime Library là tập hợp các dịch vụ mà chương trình dựa vào lúc chạy; bản chất của chúng là giả định có một OS đang hiện diện. Trên một hệ thống bình thường, đoạn code khởi động của RTL là thứ chạy trước main() - thiết lập stack, xóa (zero) vùng .bss, v.v. Chúng ta viết kernel hoàn toàn bằng C, nên ta sẽ tự cung cấp phần nhỏ mà mình thực sự cần, trong đoạn stub assembly và code khởi động của mình.\nXuyên suốt series, chúng ta sẽ xây cả hai: một runtime tối giản cho kernel C của ta những gì nó cần để chạy, và một thư viện chuẩn nhỏ lớn dần cùng chúng ta.\nSửa lỗi với QEMU + GDB Đây là câu hỏi khó nhất trong phát triển OS: khi không có printf() và không có debugger thông thường, bạn làm gì khi code không chạy?\nCâu trả lời của chúng ta là QEMU kết hợp với GDB - và đây là một trong những lợi thế lớn nhất của việc dev trên Linux. QEMU tích hợp sẵn một \u0026ldquo;GDB stub\u0026rdquo; cho phép GDB cắm thẳng vào máy ảo và debug kernel của bạn theo từng lệnh, y như một chương trình bình thường.\nQuy trình cơ bản: khởi động QEMU ở chế độ chờ debugger.\n1qemu-system-i386 -s -S -kernel kernel.bin -s - mở một GDB server tại localhost:1234 (viết tắt của -gdb tcp::1234). -S - đóng băng CPU khi khởi động và chờ lệnh của bạn trước khi chạy. Sau đó, ở một terminal thứ hai, mở GDB và kết nối:\n(gdb) target remote localhost:1234 (gdb) symbol-file kernel.elf (gdb) break kmain (gdb) continue Từ đây bạn có thể đặt breakpoint, chạy từng lệnh (stepi), xem thanh ghi (info registers), soi bộ nhớ (x/), và đọc biến. Đây là cách cực mạnh để thấy chính xác CPU đang làm gì tại mỗi thời điểm.\nQEMU mở một GDB stub qua TCP; GDB kết nối vào đó và điều khiển kernel đang bị đóng băng của bạn như thể nó là một chương trình bình thường.\nHàng thật: QEMU đóng băng ở Booting from Hard Disk... trong khi GDB đứng tại PC: 0x7c7f - chính lệnh jmp $ ở cuối boot sector, đang quay vòng tại chỗ. Mấy dòng add %al,(%eax) bên dưới chỉ là phần đệm toàn số 0 của sector bị disassemble ra.\nMẹo: đặt các lệnh kết nối đó vào một file .gdbinit để khỏi phải gõ lại mỗi phiên.\nBạn cũng nên sớm viết vài routine đơn giản để tự in thông tin ra - ra VGA text buffer, hoặc ra cổng serial. Ít nhất chúng cũng cho bạn biết code chạy được tới đâu trước khi \u0026ldquo;chết\u0026rdquo;.\nMẹo: QEMU có thể chuyển hướng cổng serial của guest thẳng ra terminal của bạn bằng -serial stdio. \u0026ldquo;In log qua serial\u0026rdquo; là một trong những mẹo debug tiện nhất khi làm OS - bạn sẽ dùng nó liên tục.\nChạy trên máy thật: ghi ra USB bằng dd Vì ta tự viết bootloader và xuất ra một ảnh đĩa flat-binary thô, việc đưa OS lên máy thật đơn giản đến bất ngờ: ghi nguyên ảnh đĩa đó ra USB bằng dd, rồi boot máy từ nó.\nTrước tiên, xác định đúng thiết bị USB. Cắm USB vào rồi chạy lsblk - bạn cần cả thiết bị (/dev/sdb, /dev/sdc, …), chứ không phải một phân vùng như /dev/sdb1:\n1lsblk Sau đó ghi ảnh đĩa:\n1sudo dd if=os-image.bin of=/dev/sdX bs=512 conv=notrunc status=progress ⚠️ Cảnh báo: dd được mệnh danh là \u0026ldquo;disk destroyer\u0026rdquo; - nó ghi đè mù quáng, không hỏi lại. Nếu bạn trỏ of= nhầm vào ổ hệ thống, toàn bộ dữ liệu trên đó sẽ mất sạch. Hãy kiểm tra /dev/sdX bằng lsblk mỗi lần, không sót lần nào.\nQuy trình hợp lý là: luôn thử trong QEMU trước - nhanh, an toàn, và có GDB - chỉ khi mọi thứ chạy ổn mới dd ra USB để kiểm tra trên phần cứng thật. Trên máy thật, nhớ bật chế độ boot Legacy/CSM trong cài đặt BIOS/UEFI và chọn USB làm thiết bị khởi động.\nTài nguyên tham khảo Xuyên suốt series, những nguồn sau sẽ vô cùng hữu ích:\nOSDev Wiki (wiki.osdev.org) - kho kiến thức phát triển OS lớn nhất; gần như câu hỏi nào của bạn cũng có lời giải ở đó. Intel® 64 and IA-32 Architectures Software Developer\u0026rsquo;s Manuals - tài liệu gốc, và là nguồn tham khảo chính xác nhất về CPU x86. Tài liệu của NASM, GNU binutils, QEMU và GDB - nắm chắc công cụ giúp bạn tiết kiệm rất nhiều thời gian. Hẹn gặp lại Vậy là xong chương mở đầu. Ở chương tiếp theo, chúng ta chính thức bước vào cuộc phiêu lưu: hệ điều hành thực sự là gì, kiến trúc của máy tính khớp với nhau ra sao, và thiết lập đầy đủ môi trường phát triển trên Ubuntu để viết những dòng code đầu tiên.\nHẹn gặp lại ở Chương 1.\n","permalink":"https://blog.minhankernel.com/vi/posts/operating-system-development-from-scratch-preface/","summary":"Chương 0: lộ trình tự xây một hệ điều hành, vì sao cross-compiler là bắt buộc, C hành xử khác gì trong cõi kernel, và cách debug kernel bằng QEMU với GDB.","tags":"os-development kernel x86 assembly c qemu","title":"Phát triển hệ điều hành từ con số 0 [Chương 0 - Lời nói đầu]"},{"content":"Process và thread là nền tảng mà gần như mọi cơ chế user-mode khác được xây trên đó, nên có một mô hình tư duy rõ ràng về chúng sẽ trả lại lợi ích trong cả debug, tối ưu hiệu năng, và bảo mật. Windows cung cấp một API khá gọn gàng cho tất cả những thứ này, nhưng chủ đề vẫn mờ mịt với phần lớn mọi người - những phần thực sự thú vị (cấu trúc kernel, scheduler, các loại process kỳ lạ) thì tài liệu vừa thưa vừa rải rác.\nBài này là chuyến đi mà tôi từng mong có được. Thay vì kể lại API reference, nó dựng khái niệm lên từ đầu, chỉ ra cấu trúc kernel đằng sau từng thứ, và - phần mà hầu hết các bài viết bỏ qua - hướng bạn tới cách thấy tất cả những điều đó trên một hệ thống đang chạy bằng WinDbg hoặc Process Explorer. Chúng ta cũng sẽ xem API fiber và job nằm ở tầng trên, và cập nhật vài chi tiết (WSL2, Windows container) mà những bài viết cũ hơn chưa kịp có.\nQuan hệ giữa Process và Thread Dưới đây là bản tóm tắt về việc chúng nằm ở đâu và liên kết với nhau thế nào. Đây cũng là cái nhìn tổng quan về cách một số cấu trúc của process và thread móc nối vào nhau.\nGiờ hãy đi vào process.\nProcess Một ngộ nhận phổ biến là program và process là cùng một thứ. Không phải. Program chỉ là một file nằm trên ổ đĩa chứa code. Còn process là một cái hộp đang sống: nó chứa một hoặc nhiều thread cùng toàn bộ tài nguyên mà những thread đó cần để chạy.\nTài nguyên của một Process Một process thực sự sở hữu những gì? Tập hợp chính xác thì khác nhau, nhưng gần như mọi process đều mang cùng những phần cốt lõi sau.\nProcess Identifier (PID) - Một con số duy nhất định danh process trên hệ thống. Hai process có thể trùng tên, nhưng không thể có hai process cùng PID tại cùng một thời điểm.\nKhông gian địa chỉ ảo riêng - Một khoảng địa chỉ ảo được dành riêng cho process sử dụng. Kích thước khác nhau tuỳ hệ thống và cấu hình.\nExecutable Code - Code được map vào không gian địa chỉ của process từ program tương ứng. Thú vị là một process có thể tồn tại mà không có code thực thi nào - Windows dùng loại process đó cho vài mục đích đặc biệt.\nHandle Table - Chứa con trỏ tới các kernel object mà process đang dùng. Những giá trị handle mà API trả về cho bạn thực chất chỉ là chỉ số trong bảng này. Nó nằm ở kernel mode, không thể chạm trực tiếp từ user mode - và nó chỉ theo dõi kernel object, không bao gồm các loại như GDI hay user object.\nAccess Token - Mỗi process mang một access token mô tả ngữ cảnh bảo mật của nó: thuộc user nào, có những privilege gì, v.v.\nProcess Environment Block (PEB) - Một cấu trúc user-mode, mỗi process một cái, chứa đầy thông tin về process - tham số dòng lệnh, có đang bị debug hay không, danh sách module đã nạp, và nhiều nữa. Microsoft không tài liệu hoá nó, nhưng nó trông như thế này:\n1struct _PEB { 2 0x000 BYTE InheritedAddressSpace; 3 0x001 BYTE ReadImageFileExecOptions; 4 0x002 BYTE BeingDebugged; 5 0x003 BYTE SpareBool; 6 0x004 void* Mutant; 7 0x008 void* ImageBaseAddress; 8 0x00c _PEB_LDR_DATA* Ldr; 9 0x010 _RTL_USER_PROCESS_PARAMETERS* ProcessParameters; 10 0x014 void* SubSystemData; 11 0x018 void* ProcessHeap; 12 0x01c _RTL_CRITICAL_SECTION* FastPebLock; 13 0x020 void* FastPebLockRoutine; 14 0x024 void* FastPebUnlockRoutine; 15 0x028 DWORD EnvironmentUpdateCount; 16 0x02c void* KernelCallbackTable; 17 0x030 DWORD SystemReserved[1]; 18 0x034 DWORD ExecuteOptions:2; 19 0x034 DWORD SpareBits:30; 20 0x038 _PEB_FREE_BLOCK* FreeList; 21 0x03c DWORD TlsExpansionCounter; 22 0x040 void* TlsBitmap; 23 0x044 DWORD TlsBitmapBits[2]; 24 0x04c void* ReadOnlySharedMemoryBase; 25 0x050 void* ReadOnlySharedMemoryHeap; 26 0x054 void** ReadOnlyStaticServerData; 27 0x058 void* AnsiCodePageData; 28 0x05c void* OemCodePageData; 29 0x060 void* UnicodeCaseTableData; 30 0x064 DWORD NumberOfProcessors; 31 0x068 DWORD NtGlobalFlag; 32 0x070 _LARGE_INTEGER CriticalSectionTimeout; 33 0x078 DWORD HeapSegmentReserve; 34 0x07c DWORD HeapSegmentCommit; 35 0x080 DWORD HeapDeCommitTotalFreeThreshold; 36 0x084 DWORD HeapDeCommitFreeBlockThreshold; 37 0x088 DWORD NumberOfHeaps; 38 0x08c DWORD MaximumNumberOfHeaps; 39 0x090 void** ProcessHeaps; 40 0x094 void* GdiSharedHandleTable; 41 0x098 void* ProcessStarterHelper; 42 0x09c DWORD GdiDCAttributeList; 43 0x0a0 void* LoaderLock; 44 0x0a4 DWORD OSMajorVersion; 45 0x0a8 DWORD OSMinorVersion; 46 0x0ac WORD OSBuildNumber; 47 0x0ae WORD OSCSDVersion; 48 0x0b0 DWORD OSPlatformId; 49 0x0b4 DWORD ImageSubsystem; 50 0x0b8 DWORD ImageSubsystemMajorVersion; 51 0x0bc DWORD ImageSubsystemMinorVersion; 52 0x0c0 DWORD ImageProcessAffinityMask; 53 0x0c4 DWORD GdiHandleBuffer[34]; 54 0x14c void (*PostProcessInitRoutine)(); 55 0x150 void* TlsExpansionBitmap; 56 0x154 DWORD TlsExpansionBitmapBits[32]; 57 0x1d4 DWORD SessionId; 58 0x1d8 _ULARGE_INTEGER AppCompatFlags; 59 0x1e0 _ULARGE_INTEGER AppCompatFlagsUser; 60 0x1e8 void* pShimData; 61 0x1ec void* AppCompatInfo; 62 0x1f0 _UNICODE_STRING CSDVersion; 63 0x1f8 void* ActivationContextData; 64 0x1fc void* ProcessAssemblyStorageMap; 65 0x200 void* SystemDefaultActivationContextData; 66 0x204 void* SystemAssemblyStorageMap; 67 0x208 DWORD MinimumStackCommit; 68}; Thread - Thực thể bên trong process thực sự chạy code. Mọi process đều bắt đầu với ít nhất một thread - primary thread. Một process không có thread nào về lý thuyết vẫn tồn tại được, nhưng chẳng có gì thực thi, nên không hữu dụng lắm.\nCấu trúc EPROCESS - Cách kernel tự biểu diễn một process object. Nó khổng lồ - chứa gần như mọi thứ kernel biết về một process. Microsoft không tài liệu hoá. Ta sẽ mổ nó ở phần dưới.\nCấu trúc KPROCESS - Lồng bên trong EPROCESS, nó mang các chi tiết liên quan tới scheduling và memory: con trỏ tới page directory của process, thời gian CPU đã tiêu ở user mode so với kernel mode, affinity, và nhiều nữa. Cũng không được tài liệu hoá. Một góc nhìn đã cắt gọn:\n1struct _KPROCESS { 2 struct _DISPATCHER_HEADER Header; 3 struct _LIST_ENTRY ProfileListHead; 4 unsigned int DirectoryTableBase; 5 unsigned long Asid; 6 struct _LIST_ENTRY ThreadListHead; 7 unsigned long ProcessLock; 8 unsigned long Spare0; 9 unsigned int DeepFreezeStartTime; 10 struct _KAFFINITY_EX Affinity; 11 struct _LIST_ENTRY ReadyListHead; 12 struct _SINGLE_LIST_ENTRY SwapListEntry; 13 struct _KAFFINITY_EX ActiveProcessors; 14 long AutoAlignment : 1; 15 long DisableBoost : 1; 16 long DisableQuantum : 1; 17 unsigned long DeepFreeze : 1; 18 unsigned long TimerVirtualization : 1; 19 unsigned long CheckStackExtents : 1; 20 unsigned long SpareFlags0 : 2; 21 unsigned long ActiveGroupsMask : 20; 22 long ReservedFlags : 4; 23 long ProcessFlags; 24 char BasePriority; 25 char QuantumReset; 26 unsigned int Visited; 27 union _KEXECUTE_OPTIONS Flags; 28 unsigned long ThreadSeed[20]; 29 unsigned int IdealNode[20]; 30 unsigned int IdealGlobalNode; 31 union _KSTACK_COUNT StackCount; 32 struct _LIST_ENTRY ProcessListEntry; 33 unsigned int CycleTime; 34 unsigned int ContextSwitches; 35 struct _KSCHEDULING_GROUP *SchedulingGroup; 36 unsigned long FreezeCount; 37 unsigned long KernelTime; 38 unsigned long UserTime; 39 void *InstrumentationCallback; 40}; Sơ đồ dưới đây gom các phần lại - tài nguyên user-mode mà một process sở hữu, và các object kernel-mode EPROCESS/KPROCESS đứng sau nó.\nTự kiểm tra lấy Mọi cấu trúc nói ở trên đều có bản đối chiếu sống mà bạn đọc trực tiếp được từ một hệ thống đang chạy. Cách làm như sau.\nWinDbg (kernel debugger) EPROCESS là bản ghi chính của kernel cho một process - tấm gương phía kernel của PEB ở user mode. Nhắm vào lsass.exe trong WinDbg là ta có ngay địa chỉ EPROCESS của nó:\n0: kd\u0026gt; !process 0 0 lsass.exe PROCESS ffffcf0514782180 SessionId: 0 Cid: 02dc Peb: c7661b7000 ParentCid: 024c DirBase: 10c761002 ObjectTable: ffff840f5f125d00 HandleCount: 1388. Image: lsass.exe Từ đó ta dump được chính cấu trúc đó. Output đã cắt bớt, chỉ giữ những field quan trọng:\nkd\u0026gt; dt nt!_eprocess ffffcf0514782180 +0x000 Pcb : _KPROCESS ... +0x4b8 Token : _EX_FAST_REF ... +0x550 Peb : 0x0000000f`a07b9000 _PEB ... +0x570 ObjectTable : 0xffff840f`5f125d00 _HANDLE_TABLE Ba field nổi bật. Pcb là KPROCESS lồng bên trong - nó giữ địa chỉ page-directory vật lý được nạp vào CR3 ở mỗi lần context switch, cùng dữ liệu scheduling: quantum, affinity, và base priority. Peb trỏ sang phía user-land, nối bản ghi kernel này về lại PEB. Còn ObjectTable là con trỏ tới Handle Table theo dõi mọi kernel object mà process đang giữ mở.\nNhìn vào bên trong KPROCESS lồng ở trong:\n0: kd\u0026gt; dt nt!_KPROCESS ffffcf0514782180 +0x000 Header : _DISPATCHER_HEADER +0x018 ProfileListHead : _LIST_ENTRY [ 0xffffcf05`14782198 - 0xffffcf05`14782198 ] +0x028 DirectoryTableBase : 0x00000001`0c761002 +0x030 ThreadListHead : _LIST_ENTRY [ 0xffffcf05`14779378 - 0xffffcf05`19c1d378 ] +0x040 ProcessLock : 0 +0x044 ProcessTimerDelay : 0 +0x048 DeepFreezeStartTime : 0 +0x050 Affinity : _KAFFINITY_EX +0x0f8 AffinityPadding : [12] 0 +0x158 ReadyListHead : _LIST_ENTRY [ 0xffffcf05`147822d8 - 0xffffcf05`147822d8 ] +0x168 SwapListEntry : _SINGLE_LIST_ENTRY +0x170 ActiveProcessors : _KAFFINITY_EX +0x218 ActiveProcessorsPadding : [12] 0 ... Access Token\nToken nằm ở EPROCESS.Token dưới dạng một con trỏ có tag. Phải mask bỏ các bit tag trước khi truyền cho !token:\n0: kd\u0026gt; dt nt!_EPROCESS ffffcf0514782180 Token.Object +0x4b8 Token : +0x000 Object : 0xffff840f`5f1ab9be Void 0: kd\u0026gt; !token @@C++(0x0xffff840f5f1ab9be \u0026amp; ~0xf) Thread is not impersonating. Using process token... _EPROCESS 0xffffcf0511067080, _TOKEN 0x0000000000000000 TS Session ID: 0 User: S-1-5-18 User Groups: 00 S-1-5-32-544 Attributes - Default Enabled Owner 01 S-1-1-0 Attributes - Mandatory Default Enabled 02 S-1-5-11 Attributes - Mandatory Default Enabled 03 S-1-16-16384 Attributes - GroupIntegrity GroupIntegrityEnabled Primary Group: S-1-5-18 Privs: 02 0x000000002 SeCreateTokenPrivilege Attributes - 03 0x000000003 SeAssignPrimaryTokenPrivilege Attributes - 04 0x000000004 SeLockMemoryPrivilege Attributes - Enabled Default 05 0x000000005 SeIncreaseQuotaPrivilege Attributes - 07 0x000000007 SeTcbPrivilege Attributes - Enabled Default 08 0x000000008 SeSecurityPrivilege Attributes - 09 0x000000009 SeTakeOwnershipPrivilege Attributes - 10 0x00000000a SeLoadDriverPrivilege Attributes - 11 0x00000000b SeSystemProfilePrivilege Attributes - Enabled Default 12 0x00000000c SeSystemtimePrivilege Attributes - 13 0x00000000d SeProfileSingleProcessPrivilege Attributes - Enabled Default 14 0x00000000e SeIncreaseBasePriorityPrivilege Attributes - Enabled Default 15 0x00000000f SeCreatePagefilePrivilege Attributes - Enabled Default 16 0x000000010 SeCreatePermanentPrivilege Attributes - Enabled Default 17 0x000000011 SeBackupPrivilege Attributes - 18 0x000000012 SeRestorePrivilege Attributes - 19 0x000000013 SeShutdownPrivilege Attributes - 20 0x000000014 SeDebugPrivilege Attributes - Enabled Default 21 0x000000015 SeAuditPrivilege Attributes - Enabled Default 22 0x000000016 SeSystemEnvironmentPrivilege Attributes - 23 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default 25 0x000000019 SeUndockPrivilege Attributes - 28 0x00000001c SeManageVolumePrivilege Attributes - 29 0x00000001d SeImpersonatePrivilege Attributes - Enabled Default 30 0x00000001e SeCreateGlobalPrivilege Attributes - Enabled Default 31 0x00000001f SeTrustedCredManAccessPrivilege Attributes - 32 0x000000020 SeRelabelPrivilege Attributes - 33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes - Enabled Default 34 0x000000022 SeTimeZonePrivilege Attributes - Enabled Default 35 0x000000023 SeCreateSymbolicLinkPrivilege Attributes - Enabled Default 36 0x000000024 SeDelegateSessionUserImpersonatePrivilege Attributes - Enabled Default Authentication ID: (0,3e7) Impersonation Level: Anonymous TokenType: Primary Source: *SYSTEM* TokenFlags: 0x2000 ( Token in use ) Token ID: 3eb ParentToken ID: 0 Modified ID: (0, 3ec) RestrictedSidCount: 0 RestrictedSids: 0x0000000000000000 OriginatingLogonSession: 0 PackageSid: (null) CapabilityCount: 0 Capabilities: 0x0000000000000000 LowboxNumberEntry: 0x0000000000000000 Security Attributes: Invalid AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION with no claims Process Token TrustLevelSid: S-1-19-1024-8192 Process Explorer (không cần debugger) Không có debugger? Process Explorer hiển thị cùng dữ liệu đó qua GUI. Khung nhìn chính liệt kê mọi process trên hệ thống - tên, PID, CPU, và memory ngay trong một cái nhìn:\nThread Thread là thực thể thực sự chạy code trên CPU. Process cung cấp tài nguyên; thread làm việc. Không có thread thì process chẳng chạy được gì, và một process chứa nhiều thread cùng lúc được gọi là multi-threaded.\nThread Scheduling Khi trên hệ thống có nhiều thread, scheduler chuyển đổi giữa chúng rất nhanh, tạo ra cảm giác rằng tất cả chạy cùng lúc. Thực tế scheduler chỉ đang nhảy qua nhảy lại giữa các thread nhanh đến mức trông như đồng thời. (Trên máy nhiều core thì một số thread thực sự có chạy song song - mỗi logical core một thread - nhưng số thread hầu như luôn nhiều hơn số core rất nhiều, nên việc chuyển đổi vẫn là chuyện chính.)\nKhoảng thời gian một thread được nhận trước khi scheduler có thể đẩy nó ra gọi là quantum, đo bằng tick đồng hồ của scheduler chứ không phải bằng giây thực. Quantum rất ngắn - vài millisecond một chữ số trên Windows bản client - đó là lý do hàng chục thread cho ta cảm giác đồng thời.\nPriority. Windows là scheduler preemptive, dựa trên priority: nó luôn chạy thread có priority cao nhất đang ở trạng thái ready, và một thread priority cao hơn vừa trở thành ready sẽ chiếm chỗ thread thấp hơn ngay giữa quantum. Priority là một số từ 0 tới 31, kết hợp priority class của process (Idle, Below Normal, Normal, Above Normal, High, Realtime) với relative priority của thread trong class đó. Vài khoảng đáng biết:\n0 - dành riêng cho zero-page thread (xoá trắng bộ nhớ đã giải phóng ở chế độ nền); không gì khác chạy ở mức 0. 1–15 - khoảng dynamic, gần như mọi thread bình thường sống ở đây. 16–31 - khoảng real-time, nằm trên phần lớn hoạt động của hệ thống và cần privilege mới vào được. Một thread real-time hỏng hóc có thể làm cả hệ thống chết đói. Priority boost. Để giữ hệ thống phản hồi tốt, Windows tạm thời đẩy priority lên khi: một thread hoàn tất I/O (để nó xử lý kết quả nhanh), các thread của cửa sổ đang ở foreground trở nên active (UI nhạy hơn), một thread thức lại sau khi chờ một event, hoặc một thread đã ready nhưng bị bỏ chờ quá lâu (chống chết đói). Boost giảm dần trở lại một mức mỗi quantum.\nThread state. Ở mỗi thời điểm một thread nằm ở một trong vài trạng thái - scheduling thực chất chỉ là việc kernel di chuyển thread giữa các trạng thái đó:\nĐường đi phổ biến là Ready → Standby → Running. Từ Running, một thread có thể bị đẩy về Ready, rơi vào Waiting trên một synchronization object hoặc I/O, hoặc kết thúc và thành Terminated. Standby nghĩa là \u0026ldquo;đã được chọn để chạy tiếp theo trên một processor cụ thể\u0026rdquo; - mỗi processor chỉ có đúng một thread ở trạng thái đó tại một thời điểm.\nTài nguyên của một Thread Process cung cấp khá nhiều, nhưng mỗi thread vẫn cần vài thứ riêng của nó.\nContext - Một cấu trúc riêng cho từng thread (do kernel quản lý) ghi lại trạng thái của toàn bộ register CPU tại lần cuối thread đó chạy. Điều này quan trọng vì mỗi CPU chỉ có thể có một thread chiếm giữ tại một thời điểm. Khi Windows chuyển thread, nó lưu trạng thái register hiện tại vào context của thread đang đi ra, và phục hồi lại khi thread đó được xếp lịch chạy lần nữa. Layout phụ thuộc processor - đây là bản x64: 1typedef struct _CONTEXT { 2 DWORD64 P1Home; 3 DWORD64 P2Home; 4 DWORD64 P3Home; 5 DWORD64 P4Home; 6 DWORD64 P5Home; 7 DWORD64 P6Home; 8 DWORD ContextFlags; 9 DWORD MxCsr; 10 WORD SegCs; 11 WORD SegDs; 12 WORD SegEs; 13 WORD SegFs; 14 WORD SegGs; 15 WORD SegSs; 16 DWORD EFlags; 17 DWORD64 Dr0; 18 DWORD64 Dr1; 19 DWORD64 Dr2; 20 DWORD64 Dr3; 21 DWORD64 Dr6; 22 DWORD64 Dr7; 23 DWORD64 Rax; 24 DWORD64 Rcx; 25 DWORD64 Rdx; 26 DWORD64 Rbx; 27 DWORD64 Rsp; 28 DWORD64 Rbp; 29 DWORD64 Rsi; 30 DWORD64 Rdi; 31 DWORD64 R8; 32 DWORD64 R9; 33 DWORD64 R10; 34 DWORD64 R11; 35 DWORD64 R12; 36 DWORD64 R13; 37 DWORD64 R14; 38 DWORD64 R15; 39 DWORD64 Rip; 40 union { 41 XMM_SAVE_AREA32 FltSave; 42 NEON128 Q[16]; 43 ULONGLONG D[32]; 44 struct { 45 M128A Header[2]; 46 M128A Legacy[8]; 47 M128A Xmm0; 48 M128A Xmm1; 49 M128A Xmm2; 50 M128A Xmm3; 51 M128A Xmm4; 52 M128A Xmm5; 53 M128A Xmm6; 54 M128A Xmm7; 55 M128A Xmm8; 56 M128A Xmm9; 57 M128A Xmm10; 58 M128A Xmm11; 59 M128A Xmm12; 60 M128A Xmm13; 61 M128A Xmm14; 62 M128A Xmm15; 63 } DUMMYSTRUCTNAME; 64 DWORD S[32]; 65 } DUMMYUNIONNAME; 66 M128A VectorRegister[26]; 67 DWORD64 VectorControl; 68 DWORD64 DebugControl; 69 DWORD64 LastBranchToRip; 70 DWORD64 LastBranchFromRip; 71 DWORD64 LastExceptionToRip; 72 DWORD64 LastExceptionFromRip; 73} CONTEXT, *PCONTEXT; Hai stack - Mỗi thread sở hữu một user-mode stack và một kernel-mode stack. User-mode stack lo phần việc thông thường như biến cục bộ. Kernel-mode stack không thể chạm tới từ user mode và đóng vai trò một ranh giới bảo mật: khi một thread thực hiện syscall, tham số của nó được copy từ user-mode stack sang kernel-mode stack. Một khi CPU đã chuyển sang kernel mode, kernel kiểm tra các tham số đó - và vì kernel stack không thể bị chạm từ user mode, thread không có cách nào can thiệp vào chúng sau khi đã kiểm tra. Sự tách biệt đó là một tính chất bảo mật then chốt.\nThread Local Storage (TLS) - Vùng lưu trữ riêng cho từng thread, dành cho dữ liệu thuộc về một thread và không nên chia sẻ với thread khác.\nThread ID (TID) - Mỗi thread có một TID duy nhất, đúng như mỗi process có một PID duy nhất.\nThread Environment Block (TEB) - Bản tương ứng ở mức thread của PEB: một cấu trúc giữ phần lớn trạng thái user-mode của một thread - con trỏ tới TLS của nó, LastErrorValue, con trỏ tới PEB, và nhiều nữa. LastErrorValue buộc phải riêng theo từng thread - nếu GetLastError của thread này thấy được mã lỗi của thread khác thì kết quả sẽ là hỗn loạn. TEB không được Microsoft tài liệu hoá.\nAffinity - Đặt affinity cho một thread là ghim nó vào một CPU cụ thể. Đặt nó vào CPU 3 trên máy bốn core thì thread sẽ chỉ chạy trên CPU 3 cho tới khi nó kết thúc hoặc affinity thay đổi.\nCấu trúc ETHREAD / KTHREAD - ETHREAD (Executive Thread) là cách kernel biểu diễn một thread object - con trỏ tới PEB, LastErrorValue, đây có phải initial thread hay không, v.v. Lồng bên trong nó, KTHREAD (Kernel Thread) mang dữ liệu scheduling: con trỏ tới kernel stack, khi nào và bao lâu thread sẽ chạy, thời gian tiêu ở user mode, và nhiều nữa. Cả hai đều không được tài liệu hoá.\nĐây là cách các thành phần của một thread ghép vào nhau, gồm cả cặp ETHREAD/KTHREAD ở kernel mode:\nTự kiểm tra lấy Phần bên trong của thread cũng quan sát được y như vậy. Một phiên kernel debugger nhắm vào cùng process lsass.exe cho ta toàn cảnh về thread.\nWinDbg (kernel debugger) Trước hết, xác nhận kích thước kernel stack mặc định - 24 KB trên hệ thống này:\n0: kd\u0026gt; dx *(int*)\u0026amp;nt!KeKernelStackSize *(int*)\u0026amp;nt!KeKernelStackSize : 24576 [Type: int] Giờ tới các cấu trúc thread. Truy vấn lsass.exe với flag 0 2 sẽ liệt kê các thread của nó và lộ ra địa chỉ ETHREAD của từng cái:\n0: kd\u0026gt; !process 0 2 lsass.exe PROCESS ffffcf0514782180 SessionId: 0 Cid: 02dc Peb: c7661b7000 ParentCid: 024c DirBase: 10c761002 ObjectTable: ffff840f5f125d00 HandleCount: 1388. Image: lsass.exe THREAD ffffcf0514779080 Cid 02dc.02ec Teb: 000000c7661bc000 Win32Thread: 0000000000000000 WAIT: (WrLpcReceive) UserMode Non-Alertable ffffcf0514779508 Semaphore Limit 0x1 Truyền địa chỉ ETHREAD đó cho !thread sẽ cho một bức ảnh đầy đủ: lý do chờ, priority, số lần context switch, biên của kernel stack, và toàn bộ call stack tại thời điểm ta lấy mẫu.\n0: kd\u0026gt; !thread ffffcf0514779080 THREAD ffffcf0514779080 Cid 02dc.02ec Teb: 000000c7661bc000 Win32Thread: 0000000000000000 WAIT: (WrLpcReceive) UserMode Non-Alertable ffffcf0514779508 Semaphore Limit 0x1 Not impersonating DeviceMap ffff840f5b635a20 Owning Process ffffcf0514782180 Image: lsass.exe Attached Process N/A Image: N/A Wait Start TickCount 17775 Ticks: 22974 (0:00:05:58.968) Context Switch Count 2 IdealProcessor: 3 UserTime 00:00:00.000 KernelTime 00:00:00.000 Win32 Start Address 0x00007ff7a99c20d0 Stack Init fffffe067d107dd0 Current fffffe067d107610 Base fffffe067d108000 Limit fffffe067d102000 Call 0000000000000000 Priority 10 BasePriority 9 IoPriority 2 PagePriority 5 Kernel stack not resident. Child-SP RetAddr : Args to Child : Call Site fffffe06`7d107650 fffff801`61cc7260 : ffffac00`ee1db180 00000000`ffffffff 00000000`00000000 00000000`00000000 : nt!KiSwapContext+0x76 fffffe06`7d107790 fffff801`61cc678f : 00000000`00000003 00000000`00000001 fffffe06`7d107950 00000000`00000000 : nt!KiSwapThread+0x500 fffffe06`7d107840 fffff801`61cc6033 : 00000000`00000000 00000000`00000000 00000000`00000000 ffffcf05`147791c0 : nt!KiCommitThreadWait+0x14f fffffe06`7d1078e0 fffff801`61d0e386 : ffffcf05`14779508 00000000`00000010 000004e8`fffffb01 000004d0`fffffb00 : nt!KeWaitForSingleObject+0x233 fffffe06`7d1079d0 fffff801`6207dcf8 : ffffffff`ffffffff 00000000`00000001 ffffcf05`1474ee20 00000000`00000000 : nt!AlpcpWaitForSingleObject+0x3e fffffe06`7d107a10 fffff801`61fe8e25 : ffffffff`00000001 fffffe06`00000000 ffffcf05`1474ee20 ffffcf05`1474ee20 : nt!AlpcpCompleteDeferSignalRequestAndWait+0x3c fffffe06`7d107a50 fffff801`61fea8bf : 000000c7`6637f6a0 00000000`00000001 00000000`00000000 fffffe06`7d107af8 : nt!AlpcpReceiveMessagePort+0x265 fffffe06`7d107ac0 fffff801`61feab2b : fffffe06`7d107ba0 000000c7`6637f6a0 00000000`00000000 00000000`00000000 : nt!AlpcpReceiveLegacyMessage+0x11f fffffe06`7d107b60 fffff801`61feabcf : ffffcf05`14779080 00000000`00000200 00000000`00000000 00000000`00000000 : nt!NtReplyWaitReceivePortEx+0xcb fffffe06`7d107c00 fffff801`61e11505 : ffffcf05`14779080 00000000`00000000 00000000`00000000 ffffcf05`00000000 : nt!NtReplyWaitReceivePort+0xf fffffe06`7d107c40 00007ffc`9f2ad704 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ fffffe06`7d107c40) 000000c7`6637f478 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtReplyWaitReceivePort+0x14 ETHREAD và KTHREAD quan hệ với thread đúng như EPROCESS và KPROCESS quan hệ với process - lần lượt là tầng Executive và tầng Kernel. Dump ETHREAD trực tiếp sẽ lộ ra các field thô:\n0: kd\u0026gt; dt nt!_ETHREAD ffffcf0514782180 +0x000 Tcb : _KTHREAD +0x430 CreateTime : _LARGE_INTEGER 0x0 +0x438 ExitTime : _LARGE_INTEGER 0x0 +0x438 KeyedWaitChain : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`000002dc ] +0x448 PostBlockList : _LIST_ENTRY [ 0xffffcf05`147dc788 - 0xffffcf05`147755c8 ] +0x448 ForwardLinkShadow : 0xffffcf05`147dc788 Void +0x450 StartAddress : 0xffffcf05`147755c8 Void +0x458 TerminationPort : (null) +0x458 ReaperLink : (null) +0x458 KeyedWaitValue : (null) +0x460 ActiveTimerListLock : 0x144d0c01`0100d000 +0x468 ActiveTimerListHead : _LIST_ENTRY [ 0x01dceea5`09511735 - 0x00000000`00005f70 ] +0x478 Cid : _CLIENT_ID +0x488 KeyedWaitSemaphore : _KSEMAPHORE +0x488 AlpcWaitSemaphore : _KSEMAPHORE +0x4a8 ClientSecurity : _PS_CLIENT_SECURITY_CONTEXT +0x4b0 IrpList : _LIST_ENTRY [ 0xffffcf05`14617310 - 0xffff840f`5f1ab9be ] +0x4c0 TopLevelIrp : 0 +0x4c8 DeviceToVerify : (null) +0x4d0 Win32StartAddress : (null) +0x4d8 ChargeOnlySession : (null) +0x4e0 LegacyPowerObject : (null) +0x4e8 ThreadListEntry : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ] +0x4f8 RundownProtect : _EX_RUNDOWN_REF +0x500 ThreadLock : _EX_PUSH_LOCK +0x508 ReadClusterSize : 0x806504a0 +0x50c MmLockOrdering : 0n-2799 +0x510 CrossThreadFlags : 0 +0x510 Terminated : 0y0 +0x510 ThreadInserted : 0y0 +0x510 HideFromDebugger : 0y0 +0x510 ActiveImpersonationInfo : 0y0 +0x510 HardErrorsAreDisabled : 0y0 ... KTHREAD lồng bên trong mang phần ruột của scheduling - con trỏ stack, cycle time, quantum, và toàn bộ trạng thái theo từng CPU mà scheduler chạm tới:\n0: kd\u0026gt; dx -id 0,0,ffffcf0511067080 -r1 (*((ntkrnlmp!_KTHREAD *)0xffffcf0514782180)) (*((ntkrnlmp!_KTHREAD *)0xffffcf0514782180)) [Type: _KTHREAD] [+0x000] Header [Type: _DISPATCHER_HEADER] [+0x018] SListFaultAddress : 0xffffcf0514782198 [Type: void *] [+0x020] QuantumTarget : 0xffffcf0514782198 [Type: unsigned __int64] [+0x028] InitialStack : 0x10c761002 [Type: void *] [+0x030] StackLimit : 0xffffcf0514779378 [Type: void *] [+0x038] StackBase : 0xffffcf0519c1d378 [Type: void *] [+0x040] ThreadLock : 0x0 [Type: unsigned __int64] [+0x048] CycleTime : 0x0 [Type: unsigned __int64] [+0x050] CurrentRunTime : 0x140001 [Type: unsigned long] [+0x054] ExpectedRunTime : 0x0 [Type: unsigned long] [+0x058] KernelStack : 0xf [Type: void *] [+0x060] StateSaveArea : 0x0 [Type: _XSAVE_FORMAT *] [+0x068] SchedulingGroup : 0x0 [Type: _KSCHEDULING_GROUP *] [+0x070] WaitRegister [Type: _KWAIT_STATUS_REGISTER] [+0x071] Running : 0x0 [Type: unsigned char] [+0x072] Alerted [Type: unsigned char [2]] [+0x074 ( 0: 0)] AutoBoostActive : 0x0 [Type: unsigned long] [+0x074 ( 1: 1)] ReadyTransition : 0x0 [Type: unsigned long] [+0x074 ( 2: 2)] WaitNext : 0x0 [Type: unsigned long] [+0x074 ( 3: 3)] SystemAffinityActive : 0x0 [Type: unsigned long] [+0x074 ( 4: 4)] Alertable : 0x0 [Type: unsigned long] [+0x074 ( 5: 5)] UserStackWalkActive : 0x0 [Type: unsigned long] [+0x074 ( 6: 6)] ApcInterruptRequest : 0x0 [Type: unsigned long] [+0x074 ( 7: 7)] QuantumEndMigrate : 0x0 [Type: unsigned long] [+0x074 ( 8: 8)] UmsDirectedSwitchEnable : 0x0 [Type: unsigned long] [+0x074 ( 9: 9)] TimerActive : 0x0 [Type: unsigned long] [+0x074 (10:10)] SystemThread : 0x0 [Type: unsigned long] [+0x074 (11:11)] ProcessDetachActive : 0x0 [Type: unsigned long] [+0x074 (12:12)] CalloutActive : 0x0 [Type: unsigned long] [+0x074 (13:13)] ScbReadyQueue : 0x0 [Type: unsigned long] [+0x074 (14:14)] ApcQueueable : 0x0 [Type: unsigned long] [+0x074 (15:15)] ReservedStackInUse : 0x0 [Type: unsigned long] [+0x074 (16:16)] UmsPerformingSyscall : 0x0 [Type: unsigned long] [+0x074 (17:17)] TimerSuspended : 0x0 [Type: unsigned long] [+0x074 (18:18)] SuspendedWaitMode : 0x0 [Type: unsigned long] [+0x074 (19:19)] SuspendSchedulerApcWait : 0x0 [Type: unsigned long] [+0x074 (20:20)] CetUserShadowStack : 0x0 [Type: unsigned long] [+0x074 (21:21)] BypassProcessFreeze : 0x0 [Type: unsigned long] [+0x074 (31:22)] Reserved : 0x0 [Type: unsigned long] ... Từ user mode (không cần kernel debugger)\nTrên x64, register segment GS luôn trỏ tới TEB của thread hiện tại, nên có thể đọc nó từ bất kỳ phiên WinDbg user-mode nào:\n// In WinDbg user-mode session: read the TEB of the current thread 0:000\u0026gt; !teb TEB at 00000025fac41000 ExceptionList: 0000000000000000 StackBase: 00000025faba0000 StackLimit: 00000025fab8f000 SubSystemTib: 0000000000000000 FiberData: 0000000000001e00 ArbitraryUserPointer: 0000000000000000 Self: 00000025fac41000 EnvironmentPointer: 0000000000000000 ClientId: 00000000000020a4 . 0000000000004c9c RpcHandle: 0000000000000000 Tls Storage: 000001d3e8e49b00 PEB Address: 00000025fac40000 LastErrorValue: 2 LastStatusValue: c0000034 Count Owned Locks: 0 HardErrorMode: 0 Process Explorer\nProperties → Threads - liệt kê mọi thread cùng TID, địa chỉ bắt đầu (đã phân giải symbol nếu PDB được nạp), thời gian CPU, và priority hiện tại. Dùng Thread Tạo một thread thì đơn giản: gọi CreateThread với địa chỉ một hàm và thread mới sẽ bắt đầu thực thi ở đó. Hàm đó là entry point của thread - nó chạy ngay khi thread bắt đầu.\n1HANDLE CreateThread( 2 [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, 3 [in] SIZE_T dwStackSize, 4 [in] LPTHREAD_START_ROUTINE lpStartAddress, 5 [in, optional] __drv_aliasesMem LPVOID lpParameter, 6 [in] DWORD dwCreationFlags, 7 [out, optional] LPDWORD lpThreadId 8); lpThreadAttributes - security attributes cho thread mới. Truyền NULL để lấy mặc định. dwStackSize - kích thước stack ban đầu. Truyền 0 để lấy mặc định. lpStartAddress - hàm entry point. lpParameter - một giá trị tuỳ ý được truyền thẳng vào entry point. dwCreationFlags - truyền 0 để chạy ngay, hoặc CREATE_SUSPENDED để bắt đầu ở trạng thái tạm dừng. lpThreadId - nhận TID của thread mới. Truyền NULL nếu bạn không cần. Một lưu ý về C runtime. CreateThread là lời gọi Win32 thô và hoạt động tốt nếu thread không bao giờ chạm tới C runtime (CRT). Nếu nó có dùng hàm CRT, hãy chọn _beginthreadex - hàm này khởi tạo và dọn dẹp phần trạng thái CRT riêng của mỗi thread mà những hàm như strtok dựa vào. Bỏ qua nó có thể làm rò rỉ trạng thái đó một cách âm thầm. _beginthreadex gọi CreateThread ở bên trong, nên bạn không mất gì cả.\nChữ ký của entry point. Hàm bạn truyền vào phải khớp LPTHREAD_START_ROUTINE: DWORD WINAPI ThreadProc(LPVOID lpParameter). Viết đúng chữ ký giúp tránh phải cast và tránh undefined behavior do sai calling convention.\nFiber Fiber là đơn vị thực thi mà bạn tự xếp lịch bằng tay, thay vì để kernel scheduler làm. Một thread có thể giữ nhiều fiber nhưng chỉ chạy một cái tại một thời điểm - và bạn chọn cái nào, một cách tường minh, bằng cách gọi SwitchToFiber. Vì chúng sống hoàn toàn trong user mode bên trong Kernel32.dll, kernel không thấy chúng chút nào.\nFiber còn đáng dùng không? Phần lớn là không. Fiber có trước các primitive async hiện đại và được coi là gần như legacy - chính Microsoft cũng ghi rằng chúng hiếm khi có lợi thế so với code multithread được thiết kế tốt. Chúng cũng tương tác tệ với C runtime và thread-local storage, vì các fiber dùng chung một thread cũng dùng chung slot TLS của thread đó (fiber có fiber-local storage riêng, FLS, qua FlsAlloc/FlsGetValue, chính là để lách chuyện này). Trừ khi bạn đang port code xây quanh một cooperative scheduler, hãy chọn thread, thread pool, hoặc async I/O. Chúng vẫn là một phương tiện rất tốt để hiểu cooperative scheduling, và đó là lý do ta nói về chúng ở đây.\nĐiểm phân biệt then chốt là ai làm việc xếp lịch. Kernel xếp lịch thread một cách preemptive; fiber được chuyển đổi một cách cooperative bởi chính code của bạn, và kernel còn không thấy được chúng:\nDùng Fiber Bắt đầu bằng cách chuyển thread hiện có của bạn thành một fiber với ConvertThreadToFiber:\n1LPVOID ConvertThreadToFiber( 2 [in, optional] LPVOID lpParameter 3); Hàm này trả về địa chỉ context của fiber mới - bạn sẽ cần nó cho SwitchToFiber về sau. Context của fiber tương tự context của thread nhưng còn giữ lpParameter, đỉnh và đáy stack của fiber, cùng vài thứ khác. Từ đây trở đi, thread của bạn chính là một fiber; nó kết thúc khi công việc của fiber xong hoặc khi nó gọi ExitThread (kết thúc cả fiber và thread).\nĐể tạo thêm fiber, gọi CreateFiber:\n1LPVOID CreateFiber( 2 [in] SIZE_T dwStackSize, 3 [in] LPFIBER_START_ROUTINE lpStartAddress, 4 [in, optional] LPVOID lpParameter 5); dwStackSize - kích thước stack của fiber; 0 dùng mặc định (nở tới 1 MB). lpStartAddress - hàm chạy khi fiber được xếp lịch. lpParameter - truyền thẳng vào hàm đó. CreateFiber chỉ tạo fiber - nó không chạy fiber. Để thực sự chuyển sang nó, gọi:\n1void SwitchToFiber( 2 [in] LPVOID lpFiber 3); Truyền địa chỉ context của fiber mà CreateFiber trả về. Việc thực thi chuyển sang ngay lập tức. Khi xong, dọn dẹp bằng:\n1void DeleteFiber( 2 [in] LPVOID lpFiber 3); Process, thread và fiber trong một cái nhìn Process Thread Fiber Là gì Hộp chứa tài nguyên Đơn vị thực thi trên CPU Đơn vị thực thi cooperative Ai xếp lịch - (chứa thread) Kernel, kiểu preemptive Bạn, bằng tay (SwitchToFiber) Sở hữu Không gian địa chỉ, handle, token Stack, context, TLS Một stack + context đã lưu Kernel thấy được? Có (EPROCESS) Có (ETHREAD) Không - thuần user-mode Số lượng 1+ mỗi hệ thống 1+ mỗi process 0+ mỗi thread, một cái chạy tại một thời điểm Tạo bằng CreateProcess CreateThread / _beginthreadex CreateFiber + SwitchToFiber Bên trong CreateProcess Khi một thread muốn dựng lên một process mới, nó gọi CreateProcess và truyền tham số để định hình process mới theo ý mình. Hàm này linh hoạt đủ để phủ gần như mọi tình huống, nhưng đôi khi một mình nó chưa đủ - nên API có thêm một họ nhỏ các hàm bọc ngoài:\nCreateProcessAsUser tạo một process thay mặt một user khác bằng cách nhận handle tới primary token của user đó. CreateProcessWithTokenW làm việc tương tự nhưng đòi một tập privilege khác. CreateProcessWithLogonW nhận thẳng credential (username, domain, password) thay vì handle của token. ShellExecute là cái khác biệt nhất. Ba hàm trước làm việc với bất kỳ file PE hợp lệ nào bất kể phần mở rộng - bạn có thể đổi tên notepad.exe thành notepad.txt và chúng vẫn chạy được. ShellExecute và ShellExecuteEx thì nhận biết loại file: chúng tra chương trình liên kết trong HKLM\\SOFTWARE\\Classes và HKCU\\SOFTWARE\\Classes, rồi cuối cùng gọi CreateProcess với đúng file thực thi và đường dẫn file làm tham số. Đưa cho nó một file .txt thì nó chạy notepad.exe filename.txt. Cả CreateProcess và CreateProcessAsUser đều được export bởi Kernel32.dll. Cả hai cuối cùng đều dồn vào CreateProcessInternal (cũng trong Kernel32.dll), rồi hàm này gọi NtCreateUserProcess trong ntdll.dll. NtCreateUserProcess là chặng cuối trong user mode - khi nó xong, nó thực hiện một syscall và vượt sang kernel mode. Cả CreateProcessInternal và NtCreateUserProcess đều không được tài liệu hoá chính thức.\nCòn CreateProcessWithTokenW và CreateProcessWithLogonW thì được export bởi Advapi32.dll. Mỗi hàm thực hiện một lời gọi RPC tới Secondary Logon Service (seclogon.dll, chạy trong svchost.exe), service này lo phần tra credential rồi cuối cùng gọi CreateProcessAsUser thay bạn.\nĐặt cả hai đường đi cạnh nhau:\nTham số CreateProcess nhận mười tham số, nhưng phần lớn là những cái núm kiểu \u0026ldquo;truyền NULL để lấy mặc định\u0026rdquo; mà MSDN đã tài liệu hoá cặn kẽ. Vài cái thực sự đáng hiểu:\nlpApplicationName so với lpCommandLine - nguồn nhầm lẫn muôn thuở. Bạn có thể ghi tên file thực thi ở một trong hai. Điểm gài: nếu bạn để lpApplicationName là NULL và truyền một tên không đủ đường dẫn trong lpCommandLine, Windows sẽ tìm kiếm qua thư mục của app, các thư mục hệ thống, thư mục hiện tại, và PATH. Dấu cách trong một đường dẫn không đặt trong ngoặc kép là nguồn kinh điển của những bug kiểu \u0026ldquo;nó chạy sai chương trình\u0026rdquo;. bInheritHandles - con có kế thừa những handle có thể kế thừa của cha hay không. Kết hợp với cờ bInheritHandle trong từng SECURITY_ATTRIBUTES, đây là cách một process cha chủ động chuyển một handle cụ thể (chẳng hạn một pipe) xuống cho process con. dwCreationFlags - hai cái bạn sẽ dùng nhiều nhất: CREATE_SUSPENDED (primary thread bắt đầu ở trạng thái dừng; resume sau bằng ResumeThread - dùng ở khắp nơi từ debugger tới việc set up job) và DEBUG_PROCESS (bên gọi trở thành debugger của process mới). lpStartupInfo - một STARTUPINFO/STARTUPINFOEX mô tả cửa sổ ban đầu của con, các std handle, và (ở dạng EX) một danh sách attribute cho các tuỳ chọn nâng cao như chọn process cha. lpProcessInformation - phần output: PID mới, TID của primary thread, và handle tới cả hai. Bạn sở hữu hai handle đó và phải CloseHandle chúng, ngay cả khi bạn không dùng tới - một chỗ rò rỉ rất dễ bỏ sót vì chương trình vẫn chạy bình thường. Các tham số về token/credential là khác biệt có ý nghĩa duy nhất giữa các thành viên trong họ hàm này. Còn lại thì giống nhau.\nChuyện gì xảy ra trước main()? CreateProcess trả về không có nghĩa là code của bạn đang chạy. Kernel map image, rồi loader (ntdll) trong process mới map mọi DLL phụ thuộc, chạy DllMain của chúng và các TLS callback, rồi CRT startup khởi tạo runtime - sau đó mới gọi entry point của bạn. Một lượng đáng ngạc nhiên các mánh của kẻ tấn công và của anti-cheat sống đúng trong khoảng thời gian giữa \u0026ldquo;process đã được tạo\u0026rdquo; và \u0026ldquo;câu lệnh đầu tiên của bạn\u0026rdquo;.\nPhân loại Process Windows định nghĩa vài loại process (phần lớn là) riêng biệt cho những trường hợp cần thêm bảo mật hoặc phục vụ mục đích đặc biệt. Chúng không được khởi chạy như process thường, và mang những thuộc tính khác.\nProtected Process Protected process ban đầu được đưa ra để thoả các yêu cầu DRM mà ngành công nghiệp nội dung đặt ra cho những thứ như HD-DVD.\nBình thường, bất kỳ process nào giữ debug privilege - thường là process được administrator khởi chạy - đều đọc và ghi được bộ nhớ của mọi process khác trên hệ thống. Điều đó hữu dụng trong nhiều tình huống, nhưng vi phạm trực tiếp yêu cầu DRM. Protected process giải quyết chuyện này: chúng cùng tồn tại với process thường nhưng cho bên ngoài rất ít hoặc không cho chút quyền truy cập nào, kể cả bên đang chạy dưới quyền administrator. Để thực sự chạy như một protected process, file thực thi phải được ký bằng Windows Media Certificate đặc biệt và chỉ nạp những DLL được ký tương tự; dữ liệu của nó chỉ kernel hoặc protected process khác chạm tới được. Trong thực tế bạn sẽ thấy điều này ở các process giải mã DRM audio/video (Audiodg.exe, Mfpmp.exe), ở protected crash-reporter (Werfaultsecure.exe - WER thường không đọc được bộ nhớ của một protected process đã crash), và ở chính process System.\nProtected Processes Light (PPL) PPL là dạng mở rộng của protected process, được tạo ra để cho phần mềm của bên thứ ba - ví dụ chương trình antivirus - có được sự bảo vệ tương tự. Điểm khác: mức độ bảo vệ của một PPL phụ thuộc vào signature level của nó, nên có PPL được bảo vệ nhiều hơn PPL khác. Nhiều process hệ thống cốt lõi của Windows được bảo vệ dạng PPL, gồm smss.exe, csrss.exe, và services.exe.\nMinimal Process Về bản chất đây là những process rỗng. Không gian địa chỉ user-mode của chúng trống trơn: không có ntdll.dll hay các DLL subsystem, không có PEB hay TEB, không có initial thread, không có image nào được map. Kernel tạo và quản lý chúng, và không có cách nào từ user mode để sinh ra một cái - chúng tồn tại cho những nhiệm vụ đặc biệt của chính hệ thống.\nMinimal process có thể chứa minimal thread, loại thread không có TEB và không có stack.\nMột ví dụ thực tế là process memory compression, nó giữ bộ nhớ đã nén của các process đang hoạt động để hệ thống có thể giữ được nhiều hơn trong RAM thay vì phải paging ra đĩa. Nó bị ẩn khỏi Task Manager vì working set của nó trông lớn đến mức đáng báo động (nó lưu bộ nhớ đã nén thuộc về các process khác), và người dùng từng thấy nó đáng nghi. Bạn có thể phát hiện nó trong Process Explorer bằng cách sắp xếp theo working set - nó sẽ nổi lên gần đầu. Nó không có thread nào và không có code riêng.\nPico Process Pico process ra đời từ một nỗ lực nghiên cứu của Microsoft có tên Project Drawbridge. Một pico process là một minimal process ghép với một driver hỗ trợ gọi là pico provider. Provider có thể quản lý gần như mọi thứ về việc thực thi của pico process - đến mức nó có thể hành xử như một kernel riêng cho process đó, và process không hề biết hệ thống bên dưới thực sự là gì. Tuy vậy, quản lý bộ nhớ, I/O, và xếp lịch thread vẫn là việc của kernel Windows thật.\nMột pico provider chặn mọi thao tác của pico process cần kernel xử lý - system call, exception, v.v. - và phản hồi theo cách nó thấy phù hợp. Pico process có thể chứa cả pico thread (minimal thread dành cho pico process) và thread thường. Pico thread mang một context lưu trong thành viên PicoContext của ETHREAD.\nWindows Subsystem for Linux WSL được xây trên mô hình pico process. Nó có thể chạy một hệ thống Linux gần như đầy đủ trên Windows mà không cần một dòng code nào của kernel Linux - điều này khả thi nhờ mức độ kiểm soát đáng kinh ngạc mà pico provider mang lại.\nPico provider của WSL là lxss.sys và lxcore.sys. Chúng mô phỏng hành vi của kernel Linux bằng cách dịch syscall Linux từ pico process của WSL sang NT API, hoặc bằng cách gọi các thành phần được viết riêng từ đầu.\nWSL1 so với WSL2 - một lưu ý quan trọng. Mọi thứ ở trên mô tả WSL1, tầng dịch dựa trên pico process. WSL2, mặc định hiện nay, hoạt động hoàn toàn khác: nó chạy một kernel Linux thật bên trong một máy ảo Hyper-V nhẹ. WSL2 không có pico process - nó đạt được khả năng tương thích syscall đầy đủ và hiệu năng tốt hơn, đổi lại mất đi sự tích hợp liền mạch mà mô hình pico đem lại cho WSL1. Nếu hôm nay bạn đang xem các process Linux, gần như chắc chắn bạn đang nhìn vào kernel Linux thật của WSL2, không phải một pico provider.\nTrustlet (Secure Process) Trustlet chạy trong Isolated User Mode (IUM), được bảo vệ bởi Virtual Trust Level của hypervisor Hyper-V - code chạy trong OS bình thường (kể cả kernel) nằm ở trust level thấp hơn và không đọc được bộ nhớ của một trustlet. Một trustlet chỉ được import một danh sách trắng nhỏ các DLL được hệ thống tin cậy (CRT, KernelBase, Advapi, RPC runtime, CNG crypto, và vài thư viện toán) - những thứ không cần syscall để hoạt động. Người dùng không tạo chúng trực tiếp được; kernel tạo, thay mặt cho các tính năng bảo mật. Ví dụ kinh điển là LSAIso, process cô lập giữ các bí mật credential cho Credential Guard.\nCác loại process trong một bảng Loại Ai tạo ra Ý chính Normal Bất kỳ ai Process thường ngày. Protected / PPL Bất kỳ ai (cần ký đặc biệt) Kể cả admin cũng không đọc được bộ nhớ của nó; mức bảo vệ do signature level quyết định. Minimal Chỉ kernel Cái vỏ rỗng - không PEB/TEB, không image, không ntdll. Ví dụ: memory compression. Pico Kernel + một driver pico provider Minimal process mà syscall/exception được một driver provider xử lý. Nền tảng của WSL1. Trustlet (IUM) Kernel, cho các tính năng bảo mật Cô lập bằng hypervisor; OS bình thường không đọc được. Ví dụ: LSAIso / Credential Guard. Job Job là cơ chế của Windows để nhóm nhiều process lại dưới một đơn vị quản lý duy nhất. Mọi giới hạn hay thay đổi áp lên một job đều ảnh hưởng tới mọi process trong đó - khiến chúng hữu dụng cho việc giới hạn tài nguyên, khoanh vùng, và theo dõi một nhóm process liên quan như một khối.\nJob object có thể chia sẻ, đặt bảo mật, và đặt tên. Một khi process đã vào job, nó không ra được. Process con do các thành viên sinh ra sẽ tự động vào cùng job, trừ khi CREATE_BREAKAWAY_FROM_JOB được truyền cho CreateProcess và job cho phép breakaway.\nHai chi tiết hiện đại mà bức tranh kinh điển bỏ qua. Thứ nhất, từ Windows 8 job có thể lồng nhau - một process có thể thuộc một hệ thống phân cấp các job, với giới hạn có hiệu lực là cái nghiêm ngặt nhất trong chuỗi. Thứ hai, bạn có thể gắn một I/O completion port vào một job (JOBOBJECT_ASSOCIATE_COMPLETION_PORT) để nhận thông báo khi process vào, ra, hoặc chạm giới hạn - tốt hơn nhiều so với polling.\nJob là nền tảng của Windows container. Silo là một job được tăng cường, có thêm khả năng ảo hoá object namespace. Server silo ảo hoá đủ phần hệ thống (registry, networking, object manager) để trông như một OS riêng - và đó chính là thứ đứng sau Windows Server Container và Docker trên Windows. Cái job object bình thường bạn sẽ tạo ở dưới cũng chính là primitive đó, chỉ được mở rộng lên tới tầm container.\nGiới hạn của Job Một số giới hạn bạn có thể đặt cho một job:\nSố process active tối đa - chặn số process có thể tồn tại trong job. Khi đã đạt mức đó, không process mới nào được gán vào và việc tạo process con sẽ thất bại. Processor affinity - giữ mọi thành viên trong một tập CPU cụ thể. Priority class - đặt priority class cho mọi thành viên. Nếu một thread cố nâng priority class của nó lên trên mức này, yêu cầu sẽ bị bỏ qua âm thầm. Giới hạn bộ nhớ ảo - chặn lượng bộ nhớ ảo mà một process, hoặc cả job, có thể commit. Đọc/ghi clipboard - cấm mọi thành viên đọc hay ghi clipboard. Các hàm API để làm việc với Job Toàn bộ API của job:\nHàm Làm gì CreateJobObject Tạo (hoặc mở) một job object có tên OpenJobObject Mở một job đã tồn tại theo tên AssignProcessToJobObject Gán một process vào một job SetInformationJobObject Áp giới hạn lên job QueryInformationJobObject Đọc thông tin và thống kê hiện tại của job TerminateJobObject Kết thúc mọi process trong job IsProcessInJob Kiểm tra một process có thuộc một job cụ thể hay không Dùng Job Quy trình là: tạo job, gán process, đặt giới hạn.\nTạo job bằng CreateJobObject:\n1HANDLE CreateJobObjectA( 2 [in, optional] LPSECURITY_ATTRIBUTES lpJobAttributes, 3 [in, optional] LPCSTR lpName 4); Truyền NULL cho cả hai tham số để lấy một job không tên với security mặc định. lpName đặt tên cho job để chia sẻ giữa các process; nếu tên đó trùng với một mutex, file-mapping object, hoặc waitable timer đã có, lời gọi sẽ thất bại.\nGán process bằng AssignProcessToJobObject:\n1BOOL AssignProcessToJobObject( 2 [in] HANDLE hJob, 3 [in] HANDLE hProcess 4); hJob là handle của job; hProcess là handle tới process bạn đang thêm vào. Dùng GetCurrentProcess() để lấy handle tới chính process đang gọi.\nÁp giới hạn bằng SetInformationJobObject:\n1BOOL SetInformationJobObject( 2 [in] HANDLE hJob, 3 [in] JOBOBJECTINFOCLASS JobObjectInformationClass, 4 [in] LPVOID lpJobObjectInformation, 5 [in] DWORD cbJobObjectInformationLength 6); JobObjectInformationClass chọn nhóm giới hạn bạn đang đặt (basic, extended, UI, v.v.); lpJobObjectInformation trỏ tới cấu trúc tương ứng.\nVí dụ thực tế: Cargo hạ cả cây process của nó thế nào Khi bạn nhấn Ctrl-C trong lúc cargo build, bạn mong cả cây process chết theo - cargo, rustc, mọi build script. Trên Unix chuyện này có sẵn: Ctrl-C gửi signal cho cả process group. Trên Windows thì không - chỉ cargo chết, để lại các process con thành mồ côi.\nCách Cargo xử lý nằm ở src/cargo/util/job.rs: tạo một Job Object, set KILL_ON_JOB_CLOSE, và gán chính cargo vào đó. Process con tự động kế thừa job, nên cả cây được nhóm lại mà không cần theo dõi PID nào.\nJob\u0026rsquo;s Cargo source code Ví dụ code Giờ khi phần nền tảng đã xong, hãy xem từng object trong số này trông thế nào trong thực tế.\nTạo một Process CreateProcess đơn giản hơn danh sách tham số của nó gợi ra - phần lớn tham số mặc định là NULL.\n1#include \u0026lt;stdio.h\u0026gt; 2#include \u0026lt;windows.h\u0026gt; 3 4int main(){ 5 STARTUPINFOA si; 6 PROCESS_INFORMATION pi; 7 8 ZeroMemory( \u0026amp;si, sizeof(si) ); 9 si.cb = sizeof(si); 10 ZeroMemory( \u0026amp;pi, sizeof(pi) ); 11 LPSTR lpCommandLine = \u0026#34;notepad.exe\u0026#34;; 12 13 if( !CreateProcessA( NULL, // No module name (use command line) 14 lpCommandLine, 15 0, // Process handle not inheritable 16 0, // Thread handle not inheritable 17 0, // No handle inheritance 18 0, // No creation flags 19 0, // Use parent\u0026#39;s environment block 20 0, // Use parent\u0026#39;s starting directory 21 \u0026amp;si, 22 \u0026amp;pi ) 23 ){ 24 printf( \u0026#34;CreateProcess failed (%d).\\n\u0026#34;, GetLastError() ); 25 return -1; 26 } 27 printf(\u0026#34;Process Created!\\n\u0026#34;); 28 29 Sleep(5000); 30 31 // Always close both handles - even if you never use them. 32 CloseHandle( pi.hProcess ); 33 CloseHandle( pi.hThread ); 34 35 return 0; 36} Đoạn này chạy notepad.exe rồi thoát sau năm giây. Hai lời gọi CloseHandle ở cuối là bắt buộc - CreateProcess luôn trả cho bạn một process handle và một thread handle, và bạn sở hữu chúng bất kể bạn có làm gì với chúng hay không.\nTạo một Thread 1#include \u0026lt;stdio.h\u0026gt; 2#include \u0026lt;windows.h\u0026gt; 3 4DWORD WINAPI EthicalFunction(LPVOID lpParam) 5{ 6 printf(\u0026#34;Thread created\\n\u0026#34;); 7 printf(\u0026#34;For educational purposes only*\\n\u0026#34;); 8 return 0; 9} 10 11int main() 12{ 13 HANDLE hThread = CreateThread(NULL, 0, EthicalFunction, NULL, 0, NULL); 14 WaitForSingleObject(hThread, INFINITE); 15 printf(\u0026#34;Thread returned\\n\u0026#34;); 16 printf(\u0026#34;Exiting...\\n\u0026#34;); 17 CloseHandle(hThread); 18 return 0; 19} EthicalFunction chạy trên thread mới trong khi main bị chặn ở WaitForSingleObject. Khi thread trả về, main chạy tiếp, in thông báo của nó, và đóng handle. Cặp WaitForSingleObject + CloseHandle đó là mức cơ bản cho bất kỳ thread nào bạn tạo.\nTạo một Fiber Fiber cần thêm một bước: bạn phải chuyển thread hiện tại thành fiber trước khi tạo thêm được.\n1#include \u0026lt;stdio.h\u0026gt; 2#include \u0026lt;windows.h\u0026gt; 3 4VOID CALLBACK fiber_function(LPVOID lpParam) 5{ 6 printf(\u0026#34;Fiber created\\n\u0026#34;); 7 printf(\u0026#34;For educational purposes only*\\n\u0026#34;); 8 // Switch back to the main fiber - fibers don\u0026#39;t return normally. 9 SwitchToFiber(lpParam); 10} 11 12int main() 13{ 14 // Step 1: convert this thread into a fiber. 15 LPVOID Context = ConvertThreadToFiber(NULL); 16 // Step 2: create another fiber, passing our context so it can switch back. 17 LPVOID lpFiber = CreateFiber(0, fiber_function, Context); 18 // Step 3: switch to it. Execution transfers immediately. 19 SwitchToFiber(lpFiber); 20 // We\u0026#39;re back. Clean up. 21 printf(\u0026#34;Fiber returned\\n\u0026#34;); 22 DeleteFiber(lpFiber); 23 printf(\u0026#34;Exiting...\\n\u0026#34;); 24 return 0; 25} Để ý rằng fiber_function không trả về - nó chuyển ngược về fiber chính một cách tường minh. Đó chính là mô hình cooperative scheduling ở dạng thu nhỏ: bạn luôn là người kiểm soát thời điểm chuyển đổi xảy ra.\nTạo một Job Object Ví dụ này tạo một job, đưa hai process vào đó ở trạng thái suspended, rồi resume chúng và kết thúc cả nhóm sau một phút.\n1#include \u0026lt;stdio.h\u0026gt; 2#include \u0026lt;windows.h\u0026gt; 3 4int main() 5{ 6 HANDLE hJob = CreateJobObject(NULL, \u0026#34;Unemployed\u0026#34;); 7 8 // Kill all processes in the job when the job handle closes. 9 JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {0}; 10 jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; 11 SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, \u0026amp;jeli, sizeof(jeli)); 12 13 STARTUPINFOA si = {0}; si.cb = sizeof(si); 14 STARTUPINFOA si1 = {0}; si1.cb = sizeof(si1); 15 PROCESS_INFORMATION pi = {0}; 16 PROCESS_INFORMATION pi1 = {0}; 17 18 // Create both processes suspended so we can assign them before they run. 19 if (!CreateProcessA(NULL, (LPSTR)\u0026#34;notepad.exe\u0026#34;, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, \u0026amp;si, \u0026amp;pi) || 20 !CreateProcessA(NULL, (LPSTR)\u0026#34;dvdplay.exe\u0026#34;, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, \u0026amp;si1, \u0026amp;pi1)) 21 { 22 printf(\u0026#34;Error creating processes: %d\\n\u0026#34;, GetLastError()); 23 return 1; 24 } 25 26 AssignProcessToJobObject(hJob, pi.hProcess); 27 AssignProcessToJobObject(hJob, pi1.hProcess); 28 29 ResumeThread(pi.hThread); 30 ResumeThread(pi1.hThread); 31 printf(\u0026#34;Job created and processes added!\\n\u0026#34;); 32 33 Sleep(60000); 34 TerminateJobObject(hJob, 0); 35 36 CloseHandle(pi.hProcess); 37 CloseHandle(pi.hThread); 38 CloseHandle(hJob); 39 40 printf(\u0026#34;Exiting...\\n\u0026#34;); 41 return 0; 42} Cờ CREATE_SUSPENDED là chi tiết then chốt: nó cho bạn một khoảng để gán process vào job trước khi chúng chạy, tránh được một race condition mà process con có thể sinh ra process cháu trước khi việc gán có hiệu lực. Để xác nhận cả hai process đều nằm trong job Unemployed, mở Process Explorer và xem Properties → Job ở notepad.exe hoặc wmplayer.exe.\nNhững cái bẫy thường gặp Vài cái bẫy hay cắn người làm việc với các API này:\nRò rỉ handle. CreateProcess trả cho bạn hai handle trong PROCESS_INFORMATION - process và thread. Quên CloseHandle cả hai là một chỗ rò rỉ chậm rất dễ bỏ sót vì chương trình vẫn chạy tốt. CreateThread và C runtime. Nếu một thread gọi hàm CRT, hãy dùng _beginthreadex thay cho CreateThread - CreateThread thô để lại trạng thái CRT riêng của thread (như buffer của strtok) chưa khởi tạo hoặc bị rò rỉ. Stack reserve cộng dồn lại. Mỗi thread mặc định dành riêng khoảng ~1 MB không gian địa chỉ. Sinh hàng nghìn thread trong một process 32-bit thì bạn sẽ cạn không gian địa chỉ từ rất lâu trước khi cạn CPU - một trong những lý do thread pool tồn tại. Priority không phải là bảo đảm. Priority cao hơn làm một thread được ưu tiên, không phải được chạy trước - boost, affinity, và những core khác đang làm gì đều góp phần. Đừng dùng priority như một cơ chế đồng bộ. Với tay tới fiber quá sớm. Chúng trông như thread giá rẻ nhưng mang theo những nguy hiểm về TLS/CRT và không cho bạn chút song song đa core nào. Mặc định hãy chọn thread, thread pool, hoặc async I/O. Đối chiếu sang Linux Nếu bạn xuất phát từ nền Unix, mô hình Windows tương ứng đại khái như sau:\nWindows Linux / POSIX Process (CreateProcess) Process (fork + execve) Thread (CreateThread) Thread / task (pthread_create, clone) Fiber (CreateFiber) ucontext (makecontext/swapcontext), green thread Thread affinity sched_setaffinity / taskset Job object cgroups (nhóm và giới hạn tài nguyên) Access token uid/gid + capabilities Handle / handle table File descriptor / fd table Khác biệt lớn nhất về mặt khái niệm: Linux làm mờ ranh giới process/thread - cả hai chỉ là \u0026ldquo;task\u0026rdquo; sinh từ clone với các cờ chia sẻ khác nhau. Windows giữ một sự phân chia cứng và tường minh giữa cái hộp process và các thread bên trong nó.\nTổng kết Ta đã đi qua bốn khối xây dựng của việc thực thi trên Windows - process (hộp chứa tài nguyên), thread (thứ mà kernel xếp lịch kiểu preemptive), fiber (đơn vị cooperative mà bạn tự xếp lịch), và job (nhóm process với giới hạn chung, và là primitive đứng sau Windows container) - cùng với các cấu trúc kernel (EPROCESS/KPROCESS, ETHREAD/KTHREAD) đứng sau chúng và những loại process đặc biệt mà Windows định nghĩa cho bảo mật và cô lập. Trên đường đi ta cũng xem scheduler thực sự quyết định chạy cái gì thế nào, CreateProcess gọi xuống kernel ra sao, và cách tạo từng object bằng code - cộng với cách quan sát tất cả trực tiếp bằng WinDbg hoặc Process Explorer.\nNếu bạn chỉ mang về ba điều: thread được xếp lịch cho bạn còn fiber được xếp lịch bởi bạn; gần như mọi cấu trúc ở đây đều kiểm tra được trên một máy đang chạy; và cái job object bạn vừa dùng để trông hai cửa sổ Notepad, khi mở rộng lên, chính là thứ chạy Windows container. Cảm ơn bạn đã đọc!\nTài liệu tham khảo Processes and Threads - Microsoft Docs CreateProcess - Microsoft Docs CreateJobObject - Microsoft Docs AssignProcessToJobObject - Microsoft Docs AssignProcessToJobObject fails with \u0026ldquo;Access Denied\u0026rdquo; under the debugger - Stack Overflow TerminateJobObject - Microsoft Docs CreateThread - Microsoft Docs CreateFiber - Microsoft Docs SwitchToFiber - Microsoft Docs Isolated User Mode (IUM) Processes - Microsoft Docs ","permalink":"https://blog.minhankernel.com/vi/posts/deep-dive-into-processes-threads-on-windows/","summary":"\u003cp\u003eProcess và thread là nền tảng mà gần như mọi cơ chế user-mode khác được xây trên đó, nên có một mô hình tư duy rõ ràng về chúng sẽ trả lại lợi ích trong cả debug, tối ưu hiệu năng, và bảo mật. Windows cung cấp một API khá gọn gàng cho tất cả những thứ này, nhưng chủ đề vẫn mờ mịt với phần lớn mọi người - những phần thực sự thú vị (cấu trúc kernel, scheduler, các loại process kỳ lạ) thì tài liệu vừa thưa vừa rải rác.\u003c/p\u003e","tags":"windows api windows internals","title":"Đào sâu vào Process, Thread, Fiber và Job trên Windows"},{"content":"Vì sao tôi tự viết framework load testing riêng Vì mọi tool tôi thử đều hoặc không test được SignalR cho đúng, hoặc tính toán sai.\nVấn đề Tôi cần load test một SignalR hub trong điều kiện thật. Hàng nghìn kết nối persistent, nhắn tin hai chiều, gọi hub thật sự.\nMọi tool tôi thử đều rơi vào một trong ba trường hợp:\nBỏ qua SignalR hoàn toàn - chỉ HTTP, không hỗ trợ WebSocket Làm giả - gửi raw WebSocket frame rồi giả vờ đó là SignalR Bắt dùng ngôn ngữ khác - viết scenario bằng JavaScript/Go/Python, rồi bằng cách nào đó đem test server .NET của mình Không cái nào nói SignalR một cách native. Không cái nào dùng đúng Microsoft.AspNetCore.SignalR.Client.\nNên tôi viết một cái làm được việc đó.\nAnPerf là gì AnPerf là một framework performance testing phân tán viết bằng .NET 8.\nScenario load test của bạn chỉ là một class C#. Không DSL. Không ngôn ngữ scripting. Không phải dịch protocol qua YAML. Bạn viết code .NET để test app .NET.\n1public class MySignalRScenario : IScenario 2{ 3 private readonly HubConnection _hub; 4 5 public MySignalRScenario(string targetUrl) 6 { 7 _hub = new HubConnectionBuilder() 8 .WithUrl($\u0026#34;{targetUrl}/hub\u0026#34;) 9 .Build(); 10 } 11 12 public async Task ExecuteAsync(IStepContext context) 13 { 14 await context.Step(\u0026#34;Connect\u0026#34;, async () =\u0026gt; 15 await _hub.StartAsync()); 16 17 await context.Step(\u0026#34;InvokeMethod\u0026#34;, async () =\u0026gt; 18 await _hub.InvokeAsync(\u0026#34;SendMessage\u0026#34;, \u0026#34;hello\u0026#34;)); 19 20 await context.Step(\u0026#34;Disconnect\u0026#34;, async () =\u0026gt; 21 await _hub.StopAsync()); 22 } 23} Chỉ vậy. SignalR client thật. Hub connection thật. Metrics thật cho từng step.\nKiến trúc AnPerf có ba tầng:\n1. Framework Engine performance cốt lõi. Lo phần scheduling, metrics, và protocol adapter.\nScheduler - closed-model (số VU cố định) và open-model (nhắm tới RPS mục tiêu), cả hai đều dùng monotonic timing Metrics - histogram sharded lock-free với 256 bucket logarithmic, zero allocation trên hot path Protocol adapter - client native cho HTTP, TCP, WebSocket, và SignalR 2. Control Plane Điều phối phân tán. Một orchestrator quản lý các agent ở xa qua WebSocket.\nProvisioning - SSH vào máy remote, deploy binary của agent, khởi động nó Job dispatch - gửi test job tới agent qua WebSocket hai chiều Result aggregation - gộp raw histogram bucket từ mọi agent, tính lại percentile 3. CLI Một tool dòng lệnh duy nhất để điều khiển mọi thứ.\n1# Add remote machines 2anperf-ctl add-host --ip 10.0.0.10 --user deploy --key ~/.ssh/id_rsa 3 4# Provision agents via SSH 5anperf-ctl provision --all 6 7# Push your scenario DLL (with content hash - only uploads once) 8anperf-ctl scenario push --name signalr-test --path ./publish/ 9 10# Run the test 11anperf-ctl run start --file job.yaml --scenario signalr-test 12 13# Watch it live 14anperf-ctl dashboard Các tuỳ chọn điều khiển: Những quyết định thiết kế đáng nói Hot path lock-free Mỗi request mà virtual user của bạn gửi đi đều đi qua đường ghi metrics. Nếu đường đó phải lấy lock, bạn đang đo overhead của chính mình - không phải hiệu năng của server.\nAnPerf dùng sharded histogram - mỗi thread ghi vào shard riêng, không tranh chấp. Các shard chỉ được gộp lại khi bạn đọc kết quả. Việc ghi là zero-allocation: không new, không boxing, không thao tác string.\nMonotonic timing Đừng bao giờ dùng DateTime.UtcNow để đo latency. Nó trôi, nó nhảy, nó nói dối.\nAnPerf dùng Stopwatch.GetTimestamp() ở mọi chỗ. Monotonic. Độ phân giải nanosecond. Không có bất ngờ nào.\nGộp percentile cho đúng Đây là chỗ hầu hết các tool làm sai.\nKhi bạn có 3 agent lần lượt báo p99 = 12ms, 14ms, 11ms - thì p99 tổng hợp không phải trung bình của chúng (12.3ms). Con số đó vô nghĩa về mặt thống kê.\nAgent của AnPerf gửi mảng raw histogram bucket (long[256]) về orchestrator. Orchestrator cộng số đếm của từng bucket theo từng phần tử, rồi tính lại percentile từ phân phối đã gộp. Đúng về mặt toán học.\nAgent 1: [0, 0, 5, 12, 45, 102, ...] // 256 buckets Agent 2: [0, 0, 3, 18, 52, 98, ...] Agent 3: [0, 0, 7, 15, 41, 110, ...] ───────────────────────────────────── Merged: [0, 0, 15, 45, 138, 310, ...] → recompute p50, p90, p99 Push scenario ≠ chạy test Upload một DLL 50MB lên 10 agent thì mất thời gian. Còn chạy test thì phải tức thì.\nAnPerf tách hai việc đó: scenario push upload qua SSH kèm dedup theo content hash (không đổi thì bỏ qua). run start chỉ gửi một lệnh WebSocket. Lần chạy thứ hai? Tức thì - không cần upload lại.\nReport từ Orchestrator Hiện tại report hỗ trợ định dạng JSON.\n1 \u0026#34;summary\u0026#34;: { 2 \u0026#34;total_requests\u0026#34;: 34861, 3 \u0026#34;total_errors\u0026#34;: 0, 4 \u0026#34;error_rate_pct\u0026#34;: 0, 5 \u0026#34;throughput_rps\u0026#34;: 1809.9, 6 \u0026#34;agent_count\u0026#34;: 1, 7 \u0026#34;total_vus\u0026#34;: 0 8 }, Một lần chạy thử bằng AnPerf (scenario E2E gồm SignalR + HTTP):\nỞ khoảng ~1.8k RPS với một agent duy nhất, hệ thống giữ được latency dưới một millisecond trên toàn bộ luồng. Các step HTTP luôn ở quanh ~0.5 ms tại p50 và dưới 1 ms tại p99, cho thấy không có tranh chấp hay xếp hàng dưới tải. Latency push của SignalR gần như tức thì (p99 ~0.013 ms), thể hiện một pipeline real-time hiệu quả, không bị dồn ứ khi phát tin.\nCó vài outlier (~40 ms) xuất hiện ở phần đuôi cực đại, nhưng chúng hiếm và không ảnh hưởng tới trải nghiệm người dùng nói chung.\nĐây là một baseline tốt cho một hệ thống real-time low-latency.\nMột lần chạy test trông như thế nào Định nghĩa workload trong YAML:\n1scenario: 2 name: \u0026#34;MySignalRScenario\u0026#34; 3 targetUrl: \u0026#34;http://target-server:5000\u0026#34; 4 5workload: 6 model: \u0026#34;closed\u0026#34; 7 stages: 8 - kind: \u0026#34;rampUp\u0026#34; 9 duration: \u0026#34;30s\u0026#34; 10 startTarget: 1 11 target: 200 12 - kind: \u0026#34;steady\u0026#34; 13 duration: \u0026#34;5m\u0026#34; 14 target: 200 15 16agents: 17 count: 3 18 labels: 19 region: \u0026#34;ap-southeast-2\u0026#34; Dashboard trực tiếp hiện mọi thứ:\n(Tôi sẽ cập nhật hình sau)\n┌─ Active Run ────────────────────────────────────────────────────────┐ │ Run: run-01 Scenario: signalr-test State: ● Running │ │ │ │ Agent Status VUs RPS Errors p99 (ms) │ │ agent-1-abc123 Running 200 4,521 3 12.4 │ │ agent-2-def456 Running 200 4,388 1 13.1 │ │ agent-3-ghi789 Running 200 4,102 0 11.8 │ │ │ │ ─── Aggregate ─────────────────────────────────────────────────── │ │ Total VUs: 600 RPS: 13,011 Errors: 4 p50: 2.1 p99: 12.8 │ └───────────────────────────────────────────────────────────────────────┘ Chi tiết theo từng agent. Metrics tổng hợp được tính đúng. Refresh real-time.\nTrạng thái hiện tại Đây là v1. Nó chạy được. Nó chạy rất tốt.\nNhưng chưa open-source.\nPhần bên trong thì chắc chắn - metrics lock-free, tổng hợp đúng, kiến trúc phân tán gọn gàng. Nhưng trải nghiệm cho người dùng thì chưa đạt mức tôi muốn. Set up vẫn còn quá nhiều bước thủ công. Tài liệu thì chưa có.\nKế hoạch: làm mượt phần setup, viết tài liệu đàng hoàng, rồi open-source khi nó đủ tốt để bất kỳ ai cũng dùng được - không chỉ riêng tôi.\nTôi học được gì Làm cái này dạy tôi về hiệu năng nhiều hơn bất kỳ bài blog hay khoá học nào:\nBạn không thể đo hiệu năng bằng một tool đang có vấn đề về hiệu năng. Hạ tầng đo phải nhanh hơn thứ bạn đem đo. Lập trình lock-free thì khó nhưng cần thiết. Một khi đã thấy tranh chấp làm gì với số đo của bạn ở quy mô lớn, bạn không thể coi như chưa thấy nữa. Tổng hợp phân tán có phần toán thật sự. Lấy trung bình percentile là sai. Lấy trung bình của trung bình cũng sai. Bạn cần dữ liệu thô. Tách bạch trách nhiệm giúp tiết kiệm thời gian. Tách upload khỏi execution, tách provisioning khỏi runtime - mỗi quyết định đều làm hệ thống đơn giản hơn và nhanh hơn. Tiếp theo là gì Làm mượt trải nghiệm CLI Viết tài liệu và hướng dẫn bắt đầu Thêm protocol adapter Open-source Nếu bạn quan tâm tới hiệu năng .NET, hệ thống phân tán, hay load testing - hãy theo dõi tiếp.\nXây bằng .NET 8, test với SignalR hub thật, và đo cho đúng.\n","permalink":"https://blog.minhankernel.com/vi/posts/write-your-own-load-testing-framework/","summary":"Vì sao tôi tự viết AnPerf - một framework performance testing phân tán hỗ trợ SignalR native, metrics lock-free, và gộp percentile đúng về mặt thống kê.","tags":"dotnet performance load-testing signalr distributed-systems","title":"Framework load testing phân tán trong .NET [Phần 1]"},{"content":"Xin chào, mình là Minh An Backend Engineer • System Enthusiast • Performance Nerd\nMình làm gì Mình xây dựng hệ thống backend - nhưng quan trọng hơn, mình cố hiểu mọi thứ thực sự vận hành thế nào.\nPhần lớn công việc của mình xoay quanh:\nThiết kế backend service (.NET, API, database) Xử lý concurrency và hệ thống real-time Làm việc với hệ thống phân tán và message queue Tối ưu hiệu năng dưới tải Nhưng mình không dừng ở tầng application.\nMình đang tìm hiểu gì Gần đây mình đi sâu hơn vào system-level engineering:\nMultithreading và synchronization Cách CPU hoạt động, cache, và memory layout Thiết kế hệ thống low-latency Linux internals và system programming C/C++ cho những đoạn code sống chết vì hiệu năng Mình đặc biệt hứng thú với những hệ thống mà từng microsecond đều có giá.\nMình quan tâm điều gì Những thứ mình coi trọng:\nHiệu năng ổn định hơn là hiệu năng trung bình Tính đoán trước được hơn là sự phức tạp Hiểu nền tảng hơn là dựa vào abstraction Vì ở quy mô lớn, những chi tiết nhỏ không còn nhỏ nữa.\nVề blog này Blog này là sổ tay kỹ thuật của mình, để công khai.\nỞ đây bạn sẽ thấy:\nNhững bài đào sâu về backend và system design Thử nghiệm và benchmark Ghi chú trong lúc học các khái niệm low-level Suy nghĩ về hiệu năng và kiến trúc Có bài sẽ còn thô. Có bài sẽ rất chi tiết. Nhưng tất cả đều phản ánh đúng những gì mình đang học.\nMục tiêu dài hạn Mình muốn trở thành một engineer có thể:\nXây hệ thống hiệu năng cao Thiết kế kiến trúc low-latency Hiểu software từ hardware → OS → application Không chỉ làm cho nó chạy được - mà làm cho nó nhanh, đoán trước được, và gọn gàng.\nLời cuối Nếu bạn cũng quan tâm tới backend engineering, system programming, hay tối ưu hiệu năng - bạn đang ở đúng nơi rồi.\nCùng làm những thứ thực sự có ý nghĩa nhé.\n","permalink":"https://blog.minhankernel.com/vi/posts/about-me/","summary":"Hành trình vào backend, hiệu năng, và những gì thực sự diễn ra bên dưới.","tags":"about-me","title":"Hello World"}]