คำอธิบาย Linux ของการเรียกระบบเคอร์เนล Man syscalls (2): การเรียกระบบ Linux ทฤษฎี. การเรียกของระบบคืออะไร


การโทรของระบบ

จนถึงตอนนี้ โปรแกรมทั้งหมดที่เราทำต้องใช้กลไกเคอร์เนลที่กำหนดไว้อย่างดีในการลงทะเบียนไฟล์ /proc และไดรเวอร์อุปกรณ์ วิธีนี้เหมาะมากหากคุณต้องการทำบางสิ่งที่โปรแกรมเมอร์เคอร์เนลจัดเตรียมไว้ให้แล้ว เช่น เขียนไดรเวอร์อุปกรณ์ แต่ถ้าคุณต้องการทำอะไรผิดปกติเปลี่ยนพฤติกรรมของระบบในทางใดทางหนึ่งล่ะ?

นี่คือจุดที่การเขียนโปรแกรมเคอร์เนลได้รับอันตราย ตอนที่เขียนตัวอย่างด้านล่าง ฉันทำลายการเรียกของระบบเปิด ซึ่งหมายความว่าฉันไม่สามารถเปิดไฟล์ใดๆ ฉันไม่สามารถรันโปรแกรมใดๆ ได้ และฉันไม่สามารถปิดระบบด้วยคำสั่งปิดระบบได้ ฉันต้องปิดเครื่องเพื่อหยุดมัน โชคดีที่ไม่มีไฟล์ถูกทำลาย เพื่อให้แน่ใจว่าคุณจะไม่สูญเสียไฟล์ โปรดทำการซิงค์ก่อนที่จะออกคำสั่ง insmod และ rmmod

ลืมไฟล์ /proc และไฟล์อุปกรณ์ไปได้เลย มันเป็นเพียงรายละเอียดเล็กๆ น้อยๆ กระบวนการสื่อสารกับเคอร์เนลที่แท้จริงซึ่งใช้โดยกระบวนการทั้งหมดคือการเรียกของระบบ เมื่อกระบวนการร้องขอบริการจากเคอร์เนล (เช่น การเปิดไฟล์ การเริ่มกระบวนการใหม่ หรือการร้องขอหน่วยความจำเพิ่มเติม) กลไกนี้จะถูกนำมาใช้ หากคุณต้องการเปลี่ยนพฤติกรรมเคอร์เนลด้วยวิธีที่น่าสนใจ นี่คือที่ที่คุณควรไป อย่างไรก็ตามหากคุณต้องการดูว่าโปรแกรมใช้การเรียกระบบใดให้รัน: strace .

โดยทั่วไปกระบวนการไม่สามารถเข้าถึงเคอร์เนลได้ ไม่สามารถเข้าถึงหน่วยความจำเคอร์เนลและไม่สามารถเรียกใช้ฟังก์ชันเคอร์เนลได้ ฮาร์ดแวร์ CPU กำหนดสถานะนี้ (มีเหตุผลที่เรียกว่า 'โหมดป้องกัน') การเรียกของระบบเป็นข้อยกเว้นสำหรับกฎทั่วไปนี้ กระบวนการจะเติมการลงทะเบียนด้วยค่าที่เหมาะสมแล้วเรียกคำสั่งพิเศษที่ข้ามไปที่ ตำแหน่งที่กำหนดไว้ล่วงหน้าในเคอร์เนล (แน่นอนว่าจะถูกอ่านโดยกระบวนการของผู้ใช้ แต่ไม่ได้เขียนทับโดยกระบวนการเหล่านี้) ภายใต้ CPU ของ Intel สามารถทำได้โดยการขัดจังหวะ 0x80 ฮาร์ดแวร์รู้ดีว่าเมื่อคุณไปถึงตำแหน่งนี้ คุณจะไม่ได้ทำงานอีกต่อไป โหมดจำกัดผู้ใช้ แต่คุณกำลังทำงานเป็นเคอร์เนลของระบบปฏิบัติการแทน ดังนั้นคุณจึงได้รับอนุญาตให้ทำทุกอย่างที่คุณต้องการ

ตำแหน่งในเคอร์เนลที่กระบวนการสามารถเรียกใช้ได้เรียกว่า system_call ขั้นตอนที่อยู่ที่นั่นจะตรวจสอบหมายเลขโทรศัพท์ของระบบ ซึ่งจะบอกเคอร์เนลว่ากระบวนการต้องการอะไรกันแน่ จากนั้นจะค้นหาตารางการเรียกของระบบ (sys_call_table) เพื่อค้นหาที่อยู่ของฟังก์ชันเคอร์เนลที่จะโทร จากนั้นจึงเรียกฟังก์ชันที่ต้องการ และหลังจากคืนค่าแล้ว จะมีการตรวจสอบระบบหลายอย่าง ผลลัพธ์จะถูกส่งกลับไปยังกระบวนการ (หรือไปยังกระบวนการอื่นหากกระบวนการสิ้นสุดลง) หากคุณต้องการดูโค้ดที่ทำหน้าที่ทั้งหมดนี้ ให้อยู่ในไฟล์อาร์ค/ซอร์ส< architecture >/kernel/entry.S หลังบรรทัด ENTRY(system_call)

ดังนั้น หากเราต้องการเปลี่ยนวิธีการทำงานของการเรียกของระบบ สิ่งแรกที่เราต้องทำคือเขียนฟังก์ชันของเราเองเพื่อทำสิ่งที่เหมาะสม (โดยปกติโดยการเพิ่มโค้ดของเราเองเล็กน้อย จากนั้นจึงเรียกใช้ฟังก์ชันดั้งเดิม) จากนั้น เปลี่ยนตัวชี้เป็น sys_call_table เพื่อชี้ไปที่ฟังก์ชันของเรา เนื่องจากเราอาจถูกลบในภายหลังและไม่ต้องการปล่อยให้ระบบอยู่ในสถานะที่ไม่สอดคล้องกัน จึงเป็นสิ่งสำคัญที่ cleanup_module จะต้องคืนค่าตารางกลับสู่สถานะดั้งเดิม

ซอร์สโค้ดที่ให้ไว้ที่นี่เป็นตัวอย่างของโมดูลดังกล่าว เราต้องการ "สอดแนม" ผู้ใช้บางราย และส่งข้อความผ่าน printk ทุกครั้งที่ผู้ใช้เปิดไฟล์ เราแทนที่การเรียกระบบเปิดไฟล์ด้วยฟังก์ชันของเราเองที่เรียกว่า our_sys_open ฟังก์ชันนี้จะตรวจสอบ uid (id ผู้ใช้) ของกระบวนการปัจจุบัน และหากเท่ากับ uid ที่เรากำลังสอดแนม ให้เรียก printk เพื่อแสดงชื่อของไฟล์ที่จะเปิด จากนั้นจะเรียกใช้ฟังก์ชัน open ดั้งเดิมด้วยพารามิเตอร์เดียวกัน ซึ่งก็คือการเปิดไฟล์จริงๆ

ฟังก์ชัน init_module จะเปลี่ยนตำแหน่งที่สอดคล้องกันใน sys_call_table และจัดเก็บตัวชี้ดั้งเดิมไว้ในตัวแปร ฟังก์ชัน cleanup_module ใช้ตัวแปรนี้เพื่อคืนค่าทุกอย่างกลับสู่สภาวะปกติ วิธีการนี้เป็นอันตรายเนื่องจากมีความเป็นไปได้ที่โมดูลทั้งสองจะแก้ไขการเรียกของระบบเดียวกัน ลองนึกภาพว่าเรามีสองโมดูล A และ B ลองเรียกการเรียกระบบเปิดของโมดูล A A_open และเรียกการเรียกเดียวกันไปยังโมดูล B B_open ตอนนี้เคอร์เนลที่แทรก syscall ถูกแทนที่ด้วย A_open ซึ่งจะเรียก sys_open ดั้งเดิมเมื่อทำสิ่งที่จำเป็นต้องทำ จากนั้น B จะถูกแทรกเข้าไปในเคอร์เนล และจะแทนที่การเรียกของระบบด้วย B_open ซึ่งจะเรียกสิ่งที่คิดว่าเป็นการเรียกของระบบดั้งเดิม แต่จริงๆ แล้วคือ A_open

ตอนนี้ถ้า B ถูกลบออกก่อน ทุกอย่างจะเรียบร้อย: มันจะคืนค่าการเรียกของระบบบน A_open ที่เรียกต้นฉบับ อย่างไรก็ตาม หาก A ถูกลบออก แล้ว B ถูกลบออก ระบบจะล่มสลาย การลบ A จะคืนค่าการเรียกของระบบกลับไปเป็นค่าเดิม sys_open โดยตัด B ออกจากลูป จากนั้นเมื่อลบ B ออก ระบบจะคืนค่าการเรียกของระบบกลับไปเป็นค่าเดิม การโทรจะถูกส่งไปยัง A_open ซึ่งไม่อยู่ในหน่วยความจำอีกต่อไป เมื่อดูเผินๆ ดูเหมือนว่าเราสามารถแก้ไขปัญหานี้ได้โดยตรวจสอบว่าการเรียกของระบบเท่ากับฟังก์ชันเปิดของเราหรือไม่ และถ้าเป็นเช่นนั้น จะไม่เปลี่ยนค่าของการเรียกนั้น (เพื่อที่ B จะไม่เปลี่ยนการเรียกของระบบเมื่อถูกลบ) ) แต่นั่นจะทำให้เกิดปัญหาที่เลวร้ายที่สุดอีกประการหนึ่ง เมื่อลบ A จะเห็นว่าการเรียกของระบบถูกเปลี่ยนเป็น B_open เพื่อไม่ให้ชี้ไปที่ A_open อีกต่อไป ดังนั้นจะไม่คืนค่าตัวชี้ไปที่ sys_open ก่อนที่จะถูกลบออกจากหน่วยความจำ น่าเสียดายที่ B_open จะยังคงพยายามเรียก A_open ซึ่งไม่อยู่ในหน่วยความจำอีกต่อไป ดังนั้นแม้จะไม่ได้ลบ B ออก ระบบก็ยังหยุดทำงาน

ฉันเห็นสองวิธีในการป้องกันปัญหานี้ ขั้นแรก: คืนค่าการเข้าถึงค่าดั้งเดิมของ sys_open ขออภัย sys_open ไม่ได้เป็นส่วนหนึ่งของตารางเคอร์เนลใน /proc/ksyms ดังนั้นเราจึงไม่สามารถเข้าถึงได้ อีกวิธีหนึ่งคือการใช้ตัวนับอ้างอิงเพื่อป้องกันไม่ให้โมดูลถูกขนถ่าย นี่เป็นสิ่งที่ดีสำหรับโมดูลทั่วไป แต่ไม่ดีสำหรับโมดูล "การศึกษา"

/* syscall.c * * การเรียกของระบบ "ขโมย" ตัวอย่าง */ /* ลิขสิทธิ์ (C) 1998-99 โดย Ori Pomerantz */ /* ไฟล์ส่วนหัวที่จำเป็น */ /* มาตรฐานในโมดูลเคอร์เนล */ #include /* เรากำลังทำงานเคอร์เนลอยู่ */ #include /* โดยเฉพาะโมดูล */ /* จัดการกับ CONFIG_MODVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include #endif #รวม /* รายการการเรียกของระบบ */ /* สำหรับโครงสร้างปัจจุบัน (กระบวนการ) เราต้องการ * สิ่งนี้เพื่อทราบว่าใครคือผู้ใช้ปัจจุบัน */ #รวม /* ใน 2.2.3 /usr/include/linux/version.h มี * มาโครสำหรับสิ่งนี้ แต่ 2.0.35 ไม่มี - ดังนั้นฉันจึงเพิ่ม * ที่นี่หากจำเป็น */ #ifndef KERNEL_VERSION #define KERNEL_VERSION(a ,b,c) ((a)*65536+(b)*256+(c)) #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #include #endif /* ตารางการเรียกระบบ (ตารางฟังก์ชัน) เรา * เพียงกำหนดสิ่งนี้เป็นภายนอกและเคอร์เนลจะ * เติมมันให้เราเมื่อเรา insmod"ed */ extern void *sys_call_table; /* UID ที่เราต้องการสอดแนม - จะถูกกรอกจากบรรทัดคำสั่ง * */ int uid; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) MODULE_PARM(uid, "i"); #endif /* ตัวชี้ไปยังการเรียกระบบดั้งเดิม * (sys_open) เป็นเพราะบุคคลอื่นอาจมี * แทนที่ การเรียกของระบบต่อหน้าเรา โปรดทราบว่า * นี้จะไม่ปลอดภัย 100% เพราะหากโมดูลอื่น * แทนที่ sys_open ก่อนเรา เมื่อเราแทรก * เราจะเรียกใช้ฟังก์ชันในโมดูลนั้น - และ * อาจถูกลบออกก่อน เราเป็นเช่นนั้น * * อีกเหตุผลหนึ่งคือเราไม่สามารถรับ sys_open ได้ .. */ printk("ฉันอันตราย ฉันหวังว่าคุณจะทำ "); printk("sync before you insmod"ed me.\n");

ส่วนใหญ่แล้ว รหัสการโทรของระบบที่มีหมายเลข __NR_xxx กำหนดไว้ /usr/include/asm/unistd.hสามารถพบได้ในซอร์สโค้ดเคอร์เนล Linux ในฟังก์ชัน sys_xxx- (สามารถดูตารางการโทรสำหรับ i386 ได้ใน /usr/src/linux/arch/i386/kernel/entry.S.) มีข้อยกเว้นหลายประการสำหรับกฎนี้ สาเหตุหลักมาจากการที่การเรียกระบบเก่าส่วนใหญ่จะถูกแทนที่ด้วยการเรียกใหม่ โดยไม่มีระบบใดๆ บนแพลตฟอร์มการจำลองระบบปฏิบัติการที่เป็นกรรมสิทธิ์ เช่น parisc, sparc, sparc64 และ alpha มีการเรียกระบบเพิ่มเติมมากมาย mips64 ยังมีการเรียกระบบ 32 บิตครบชุด

เมื่อเวลาผ่านไป มีการเปลี่ยนแปลงเกิดขึ้นในส่วนต่อประสานของการเรียกระบบบางอย่าง หากจำเป็น สาเหตุหนึ่งของการเปลี่ยนแปลงเหล่านี้คือความจำเป็นในการเพิ่มขนาดของโครงสร้างหรือค่าสเกลาร์ที่ส่งไปยังการเรียกของระบบ เนื่องจากการเปลี่ยนแปลงเหล่านี้ กลุ่มต่างๆ ของการเรียกของระบบที่คล้ายกันจึงปรากฏขึ้นบนสถาปัตยกรรมบางตัว (เช่น i386 แบบ 32 บิตที่เก่ากว่า) (เช่น ตัด(2) และ ตัดทอน 64(2)) ซึ่งทำงานเดียวกัน แต่มีขนาดข้อโต้แย้งต่างกัน (ตามที่ระบุไว้ สิ่งนี้จะไม่ส่งผลกระทบต่อแอปพลิเคชัน: ฟังก์ชัน glibc wrapper จะทำงานบางอย่างเพื่อทริกเกอร์การเรียกของระบบที่ถูกต้อง และสิ่งนี้ทำให้แน่ใจได้ถึงความเข้ากันได้ของ ABI สำหรับไบนารีรุ่นเก่า) ตัวอย่างของการเรียกของระบบที่มีหลายเวอร์ชัน:

*ปัจจุบันมีสามเวอร์ชันที่แตกต่างกัน สถิติ(2): sys_stat() (สถานที่ __NR_oldstat), sys_newstat() (สถานที่ __NR_stat) และ sys_stat64() (สถานที่ __NR_stat64) แบบหลังมีการใช้งานอยู่ในปัจจุบัน สถานการณ์ที่คล้ายกันกับ lstat(2) และ fstat(2). * กำหนดในทำนองเดียวกัน, __NR_olduname __NR_olduname และ __NR_uname สำหรับการโทร(), sys_olduname sys_uname () และ sys_newuname - * Linux 2.0 มีเวอร์ชั่นใหม่แล้ว vm86 (2) เรียกว่ากระบวนการนิวเคลียร์เวอร์ชันใหม่และเก่า sys_vm86old () และ sys_vm86 - * Linux 2.4 มีเวอร์ชั่นใหม่แล้ว getrlimit sys_old_getrlimit() (สถานที่ __NR_getrlimit) และ sys_getrlimit() (สถานที่ __NR_ugetrlimit- * ใน Linux 2.4 ขนาดของฟิลด์ ID ผู้ใช้และกลุ่มเพิ่มขึ้นจาก 16 เป็น 32 บิต มีการเพิ่มการเรียกระบบหลายครั้งเพื่อรองรับการเปลี่ยนแปลงนี้ (เช่น ชัช32(2), getuid32(2), รับกลุ่ม32(2), ชุดอุปกรณ์32(2)) กำจัดการโทรก่อนหน้านี้ด้วยชื่อเดียวกัน แต่ไม่มีส่วนต่อท้าย "32" * Linux 2.4 เพิ่มการรองรับสำหรับการเข้าถึงไฟล์ขนาดใหญ่ (ซึ่งมีขนาดและออฟเซ็ตไม่พอดีกับ 32 บิต) ในแอปพลิเคชันบนสถาปัตยกรรม 32 บิต สิ่งนี้จำเป็นต้องเปลี่ยนแปลงการเรียกของระบบที่ทำงานกับขนาดไฟล์และออฟเซ็ต เพิ่มการเรียกระบบต่อไปนี้:(2), fcntl64(2), getdents64(2), สถิติ64(2), ตัดทอน 64สตาฟส์64

(2) และแอนะล็อกซึ่งจัดการตัวอธิบายไฟล์หรือลิงก์สัญลักษณ์ การเรียกของระบบเหล่านี้จะกำจัดการเรียกของระบบแบบเก่า ซึ่งมีการตั้งชื่อเช่นกัน แต่ไม่มีส่วนต่อท้าย "64" ยกเว้นการเรียก "stat"

บนแพลตฟอร์มรุ่นใหม่ที่มีการเข้าถึงไฟล์ 64 บิตและ UID/GID 32 บิต (เช่น alpha, ia64, s390x, x86-64) มีการเรียกระบบเพียงเวอร์ชันเดียวสำหรับ UID/GID และการเข้าถึงไฟล์ บนแพลตฟอร์ม (โดยปกติจะเป็นแพลตฟอร์ม 32 บิต) ที่มีการเรียกใช้ *64 และ *32 เวอร์ชันอื่นๆ จะล้าสมัย * ความท้าทาย rt_sig* เพิ่มในเคอร์เนล 2.2 เพื่อรองรับสัญญาณเรียลไทม์เพิ่มเติม (ดูเคอร์เนล 2.2)สัญญาณ (7)). การเรียกของระบบเหล่านี้จะแทนที่การเรียกของระบบเก่าที่มีชื่อเดียวกัน แต่ไม่มีคำนำหน้า "rt_"(2) และ * ในการโทรของระบบเลือก แมป sys_vm86old (2) ใช้อาร์กิวเมนต์ตั้งแต่ห้าอาร์กิวเมนต์ขึ้นไป ซึ่งทำให้เกิดปัญหาในการพิจารณาว่าอาร์กิวเมนต์ถูกส่งผ่านบน i386 อย่างไร ด้วยเหตุนี้ ในขณะที่สถาปัตยกรรมอื่นๆ เรียก sys_select sys_mmap() จับคู่ __NR_เลือกและ __NR_mmap sys_vm86old บน i386 พวกเขาสอดคล้องกัน old_select old_mmap() (ขั้นตอนที่ใช้ตัวชี้ไปยังบล็อกอาร์กิวเมนต์) ขณะนี้ไม่มีปัญหาในการส่งผ่านข้อโต้แย้งมากกว่าห้าข้ออีกต่อไปและมี แมป __NR__เลือกข่าว ซึ่งสอดคล้องกันทุกประการ.

() และสถานการณ์เดียวกันกับ
__NR_mmap2

วอลรัสพูดหลายเรื่องว่า “ถึงเวลาต้องพูดแล้ว”

มีการเขียนและเขียนใหม่มากมายในหัวข้อโครงสร้างภายในของเคอร์เนล Linux โดยทั่วไป ระบบย่อยต่างๆ และการเรียกระบบโดยเฉพาะ ผู้เขียนที่เคารพตนเองทุกคนควรเขียนเกี่ยวกับเรื่องนี้อย่างน้อยหนึ่งครั้ง เช่นเดียวกับที่โปรแกรมเมอร์ที่เคารพตนเองทุกคนควรเขียนโปรแกรมจัดการไฟล์ของตัวเองอย่างแน่นอน :) แม้ว่าฉันจะไม่ใช่นักเขียนด้านไอทีมืออาชีพ แต่โดยทั่วไปแล้ว ฉันจะจดบันทึกเป็นครั้งแรกโดยเฉพาะ เพื่อไม่ให้ลืมสิ่งที่เรียนมาเร็วเกินไป แต่ถ้าบันทึกการเดินทางของฉันมีประโยชน์กับใครบางคน แน่นอนว่าฉันคงมีความสุขเท่านั้น โดยทั่วไปแล้ว คุณไม่สามารถทำให้โจ๊กเสียด้วยน้ำมันได้ ดังนั้นบางทีฉันอาจจะเขียนหรืออธิบายบางสิ่งที่ไม่มีใครสนใจที่จะพูดถึงได้

ทฤษฎี. การเรียกของระบบคืออะไร?

เมื่อพวกเขาอธิบายให้ผู้ที่ไม่ได้ฝึกหัดฟังว่าซอฟต์แวร์ (หรือระบบปฏิบัติการ) คืออะไร พวกเขามักจะพูดดังนี้: ตัวคอมพิวเตอร์เองก็เป็นเพียงส่วนหนึ่งของฮาร์ดแวร์ แต่ซอฟต์แวร์คือสิ่งที่ช่วยให้คุณได้รับประโยชน์จากฮาร์ดแวร์ชิ้นนี้ แน่นอนว่าหยาบ แต่โดยทั่วไปแล้วค่อนข้างจริง ฉันอาจจะพูดแบบเดียวกันเกี่ยวกับระบบปฏิบัติการและการเรียกของระบบ ในความเป็นจริงในระบบปฏิบัติการที่แตกต่างกันการเรียกของระบบสามารถใช้งานได้แตกต่างกัน จำนวนการเรียกเหล่านี้อาจแตกต่างกันไป แต่อย่างใดในรูปแบบใดรูปแบบหนึ่งกลไกการโทรของระบบนั้นมีอยู่ในระบบปฏิบัติการใด ๆ ทุกวัน ผู้ใช้ทำงานกับไฟล์ทั้งโดยชัดแจ้งหรือโดยปริยาย แน่นอนว่าเขาสามารถเปิดไฟล์เพื่อแก้ไขใน MS Word หรือ Notepad ที่เขาชื่นชอบได้อย่างชัดเจนหรือเขาสามารถเปิดของเล่นก็ได้ซึ่งอิมเมจที่ปฏิบัติการได้นั้นจะถูกเก็บไว้ในไฟล์ด้วยซึ่งในทางกลับกันจะต้อง ถูกเปิดและอ่านโดยไฟล์ปฏิบัติการ bootloader ในทางกลับกัน ของเล่นยังสามารถเปิดและอ่านไฟล์ได้หลายสิบไฟล์ระหว่างการทำงาน โดยปกติแล้วไฟล์ไม่เพียงแต่สามารถอ่านได้ แต่ยังเขียนได้อีกด้วย (อย่างไรก็ตามไม่เสมอไป แต่ที่นี่เราไม่ได้พูดถึงการแยกสิทธิ์และการเข้าถึงแบบแยกส่วน :)) ทั้งหมดนี้ได้รับการจัดการโดยเคอร์เนล (ในระบบปฏิบัติการไมโครเคอร์เนลสถานการณ์อาจแตกต่างกัน แต่ตอนนี้เราจะย้ายไปยังหัวข้อการสนทนาของเราอย่างสงบเสงี่ยม - Linux ดังนั้นเราจะเพิกเฉยต่อประเด็นนี้) การสร้างกระบวนการใหม่นั้นเป็นบริการที่เคอร์เนลระบบปฏิบัติการมอบให้ด้วย ทั้งหมดนี้ยอดเยี่ยมเช่นเดียวกับความจริงที่ว่าโปรเซสเซอร์สมัยใหม่ทำงานที่ความถี่ในช่วงกิกะเฮิรตซ์และประกอบด้วยทรานซิสเตอร์หลายล้านตัว แต่จะทำอย่างไรต่อไป ใช่ จะเกิดอะไรขึ้นหากไม่มีกลไกที่แอปพลิเคชันของผู้ใช้สามารถทำงานได้ค่อนข้างธรรมดาและในขณะเดียวกันก็ทำสิ่งที่จำเป็น ( ในความเป็นจริงการกระทำเล็กน้อยเหล่านี้ไม่ว่าในกรณีใดไม่ได้ดำเนินการโดยแอปพลิเคชันผู้ใช้ แต่โดยเคอร์เนลระบบปฏิบัติการ - ผู้เขียน) จากนั้นระบบปฏิบัติการก็เป็นเพียงสิ่งหนึ่งในตัวเอง - ไร้ประโยชน์อย่างแน่นอนหรือในทางกลับกันแอปพลิเคชันผู้ใช้แต่ละตัวจะต้องกลายเป็นระบบปฏิบัติการเพื่อตอบสนองทุกความต้องการอย่างอิสระ น่ารักใช่มั้ยล่ะ?

ดังนั้นเราจึงมาถึงคำจำกัดความของการเรียกของระบบในการประมาณครั้งแรก: การเรียกระบบเป็นบริการบางอย่างที่เคอร์เนลระบบปฏิบัติการมอบให้กับแอปพลิเคชันผู้ใช้ตามคำขอของฝ่ายหลัง บริการดังกล่าวอาจเป็นการเปิดไฟล์ที่กล่าวถึงแล้ว, การสร้าง, การอ่าน, การเขียน, การสร้างกระบวนการใหม่, การได้รับตัวระบุกระบวนการ (pid), การติดตั้งระบบไฟล์, การหยุดระบบในที่สุด ในชีวิตจริง มีการเรียกของระบบมากกว่าที่ระบุไว้ที่นี่

การเรียกของระบบมีลักษณะอย่างไรและมันคืออะไร? จากที่กล่าวไว้ข้างต้น เห็นได้ชัดว่าการเรียกระบบเป็นรูทีนย่อยเคอร์เนลที่มีรูปแบบที่สอดคล้องกัน ผู้ที่เคยมีประสบการณ์ในการเขียนโปรแกรมภายใต้ Win9x/DOS อาจจะจำ int 0x21 Interrupt ได้ด้วยฟังก์ชันทั้งหมด (หรืออย่างน้อยบางส่วน) ของมัน อย่างไรก็ตาม มีนิสัยใจคอเล็กๆ น้อยๆ อย่างหนึ่งที่ใช้ได้กับการโทรระบบ Unix ทั้งหมด ตามหลักการแล้ว ฟังก์ชันที่ใช้การเรียกของระบบสามารถรับอาร์กิวเมนต์ได้ N หรือไม่มีเลย แต่อย่างใด ฟังก์ชันจะต้องส่งคืนค่า int ไม่ทางใดก็ทางหนึ่ง ค่าที่ไม่เป็นลบใดๆ จะถูกตีความว่าเป็นการดำเนินการฟังก์ชันการเรียกของระบบได้สำเร็จ และด้วยเหตุนี้การเรียกระบบจึงเกิดขึ้นเอง ค่าที่น้อยกว่าศูนย์ถือเป็นสัญญาณของข้อผิดพลาด และในเวลาเดียวกันก็มีรหัสข้อผิดพลาด (รหัสข้อผิดพลาดถูกกำหนดไว้ในส่วนหัว include/asm-generic/errno-base.h และ include/asm-generic/errno.h) . ใน Linux เกตเวย์สำหรับการเรียกของระบบจนกระทั่งเมื่อเร็วๆ นี้คือการขัดจังหวะ int 0x80 ในขณะที่ใน Windows (จนถึงเวอร์ชัน XP Service Pack 2 หากฉันจำไม่ผิด) เกตเวย์คือการขัดจังหวะ 0x2e อีกครั้งในเคอร์เนล Linux จนกระทั่งเมื่อเร็วๆ นี้ การเรียกระบบทั้งหมดได้รับการจัดการโดยฟังก์ชัน system_call() อย่างไรก็ตาม เมื่อปรากฏในภายหลัง กลไกแบบคลาสสิกสำหรับการประมวลผลการเรียกระบบผ่านเกตเวย์ 0x80 ทำให้ประสิทธิภาพลดลงอย่างมากบนโปรเซสเซอร์ Intel Pentium 4 ดังนั้นกลไกแบบคลาสสิกจึงถูกแทนที่ด้วยวิธีการของวัตถุที่ใช้ร่วมกันแบบไดนามิกเสมือน (DSO - ไดนามิก) ไฟล์อ็อบเจ็กต์ที่แชร์ ฉันไม่สามารถรับรองการแปลที่ถูกต้องได้ แต่ DSO คือสิ่งที่ผู้ใช้ Windows รู้จักในชื่อ DLL - ไลบรารีที่โหลดแบบไดนามิกและเชื่อมโยง) - VDSO อะไรคือความแตกต่างระหว่างวิธีการใหม่กับวิธีการแบบคลาสสิก? ก่อนอื่น เรามาดูวิธีการแบบคลาสสิกซึ่งทำงานผ่านเกต 0x80

กลไกแบบคลาสสิกสำหรับการให้บริการการเรียกระบบใน Linux

ขัดจังหวะในสถาปัตยกรรม x86

ตามที่กล่าวไว้ข้างต้น ก่อนหน้านี้ เกตเวย์ 0x80 (int 0x80) ถูกใช้เพื่อให้บริการคำขอจากแอปพลิเคชันของผู้ใช้ การทำงานของระบบที่ใช้สถาปัตยกรรม IA-32 ถูกควบคุมโดยการขัดจังหวะ (พูดอย่างเคร่งครัด ซึ่งใช้ได้กับระบบที่ใช้ x86 โดยทั่วไป) เมื่อมีเหตุการณ์บางอย่างเกิดขึ้น (เครื่องหมายตัวจับเวลาใหม่ กิจกรรมบางอย่างบนอุปกรณ์บางอย่าง ข้อผิดพลาด - การหารด้วยศูนย์ ฯลฯ) การขัดจังหวะจะถูกสร้างขึ้น การขัดจังหวะนั้นถูกตั้งชื่อเช่นนี้เพราะโดยทั่วไปแล้วมันจะขัดจังหวะโฟลว์โค้ดปกติ การขัดจังหวะมักแบ่งออกเป็นการขัดจังหวะฮาร์ดแวร์และซอฟต์แวร์ การขัดจังหวะด้วยฮาร์ดแวร์คือการขัดจังหวะที่สร้างขึ้นโดยระบบและอุปกรณ์ต่อพ่วง เมื่ออุปกรณ์จำเป็นต้องดึงดูดความสนใจของเคอร์เนลระบบปฏิบัติการ อุปกรณ์นั้น (อุปกรณ์) จะสร้างสัญญาณบนบรรทัดคำขอขัดจังหวะ (IRQ - บรรทัดคำขอขัดจังหวะ) สิ่งนี้นำไปสู่ความจริงที่ว่าสัญญาณที่เกี่ยวข้องถูกสร้างขึ้นที่อินพุตของโปรเซสเซอร์บางตัวโดยที่โปรเซสเซอร์ตัดสินใจขัดจังหวะการดำเนินการของสตรีมคำสั่งและถ่ายโอนการควบคุมไปยังตัวจัดการการขัดจังหวะซึ่งค้นพบแล้วว่าเกิดอะไรขึ้นและต้องทำอะไร จะทำ การขัดจังหวะด้วยฮาร์ดแวร์มีลักษณะไม่ตรงกัน ซึ่งหมายความว่าการหยุดชะงักสามารถเกิดขึ้นได้ตลอดเวลา นอกเหนือจากอุปกรณ์ต่อพ่วงแล้ว ตัวประมวลผลเองก็สามารถสร้างการขัดจังหวะได้ (หรือที่แม่นยำยิ่งขึ้น ข้อยกเว้นของฮาร์ดแวร์ - ข้อยกเว้นของฮาร์ดแวร์ - ตัวอย่างเช่น การหารด้วยศูนย์ที่กล่าวถึงแล้ว) เพื่อแจ้งให้ OS ทราบถึงสถานการณ์ผิดปกติที่เกิดขึ้น เพื่อให้ OS สามารถดำเนินการบางอย่างเพื่อตอบสนองต่อการเกิดสถานการณ์ดังกล่าวได้ หลังจากประมวลผลการขัดจังหวะแล้ว ตัวประมวลผลจะกลับไปดำเนินการโปรแกรมที่ถูกขัดจังหวะอีกครั้ง การขัดจังหวะสามารถเริ่มต้นได้โดยแอปพลิเคชันของผู้ใช้ การขัดจังหวะประเภทนี้เรียกว่าซอฟต์แวร์ขัดจังหวะ การขัดจังหวะของซอฟต์แวร์นั้นต่างจากฮาร์ดแวร์ตรงที่เป็นแบบซิงโครนัส นั่นคือเมื่อมีการเรียกอินเทอร์รัปต์ โค้ดที่เรียกจะหยุดชั่วคราวจนกว่าจะได้รับบริการอินเทอร์รัปต์ เมื่อออกจากตัวจัดการการขัดจังหวะ มันจะกลับไปยังที่อยู่ที่ไกลที่สุดที่เก็บไว้ก่อนหน้า (เมื่อมีการเรียกใช้การขัดจังหวะ) บนสแต็ก ไปยังคำสั่งถัดไปหลังจากคำสั่งการขัดจังหวะ (int) ตัวจัดการการขัดจังหวะคือส่วนของโค้ดที่มีถิ่นที่อยู่ (อยู่ในหน่วยความจำอย่างถาวร) ตามกฎแล้วนี่เป็นโปรแกรมขนาดเล็ก แม้ว่าถ้าเราพูดถึงเคอร์เนล Linux ตัวจัดการการขัดจังหวะก็ไม่ได้เล็กเสมอไป ตัวจัดการขัดจังหวะถูกกำหนดโดยเวกเตอร์ เวกเตอร์ไม่มีอะไรมากไปกว่าที่อยู่ (ส่วนและออฟเซ็ต) ของการเริ่มต้นโค้ดที่ควรจัดการการขัดจังหวะที่ดัชนีที่กำหนด การทำงานกับอินเทอร์รัปต์จะแตกต่างกันอย่างมากในโหมดการทำงานของโปรเซสเซอร์จริง (โหมดจริง) และที่ได้รับการป้องกัน (โหมดป้องกัน) (ฉันขอเตือนคุณว่าต่อไปนี้เราหมายถึงโปรเซสเซอร์ Intel และโปรเซสเซอร์ที่เข้ากันได้กับโปรเซสเซอร์เหล่านี้) ในโหมดการทำงานของโปรเซสเซอร์จริง (ไม่มีการป้องกัน) ตัวจัดการการขัดจังหวะจะถูกกำหนดโดยเวกเตอร์ ซึ่งจะถูกเก็บไว้ที่จุดเริ่มต้นของหน่วยความจำเสมอ ที่อยู่ที่ต้องการจะถูกเลือกจากตารางเวกเตอร์โดยใช้ดัชนี ซึ่งเป็นหมายเลขขัดจังหวะด้วย ด้วยการเขียนทับเวกเตอร์ด้วยดัชนีที่แน่นอน คุณสามารถกำหนดตัวจัดการของคุณเองให้กับอินเทอร์รัปต์ได้

ในโหมดที่ได้รับการป้องกัน ตัวจัดการการขัดจังหวะ (เกต เกต หรือเกต) จะไม่ถูกกำหนดโดยใช้ตารางเวกเตอร์อีกต่อไป แทนที่จะเป็นตารางนี้ จะใช้ตารางเกทหรือตารางขัดจังหวะ - IDT (ตารางตัวอธิบายการขัดจังหวะ) ตารางนี้สร้างขึ้นโดยเคอร์เนล และที่อยู่ของตารางนี้จะถูกจัดเก็บไว้ในการลงทะเบียน idtr ของตัวประมวลผล การลงทะเบียนนี้ไม่สามารถเข้าถึงได้โดยตรง การทำงานกับมันสามารถทำได้โดยใช้คำสั่ง lidt/sidt เท่านั้น อันแรก (lidt) โหลดการลงทะเบียน idtr ด้วยค่าที่ระบุในตัวถูกดำเนินการและเป็นที่อยู่ฐานของตารางตัวอธิบายการขัดจังหวะ ส่วนที่สอง (sidt) เก็บที่อยู่ตารางที่อยู่ใน idtr ลงในตัวถูกดำเนินการที่ระบุ ในทำนองเดียวกับที่ข้อมูลเกี่ยวกับเซ็กเมนต์ถูกดึงมาจากตารางตัวอธิบายโดยใช้ตัวเลือก ตัวอธิบายเซ็กเมนต์ที่ทำหน้าที่ขัดจังหวะในโหมดที่ได้รับการป้องกันก็จะถูกดึงออกมาเช่นกัน การป้องกันหน่วยความจำได้รับการสนับสนุนโดยโปรเซสเซอร์ Intel ที่เริ่มต้นด้วย CPU i80286 (ไม่ตรงกับรูปแบบที่นำเสนอในขณะนี้หากเพียงเพราะ 286 เป็นโปรเซสเซอร์ 16 บิต - ดังนั้น Linux จึงไม่สามารถทำงานบนโปรเซสเซอร์เหล่านี้ได้) และ i80386 และดังนั้น ตัวประมวลผลเองทำการเลือกที่จำเป็นทั้งหมด ดังนั้น เราจะไม่เจาะลึกถึงความซับซ้อนทั้งหมดของโหมดที่ได้รับการป้องกัน (กล่าวคือ Linux ทำงานในโหมดที่ได้รับการป้องกัน) น่าเสียดายที่ไม่มีเวลาหรือความสามารถใดที่ทำให้เราจมอยู่กับกลไกในการจัดการกับการหยุดชะงักในโหมดที่ได้รับการป้องกันเป็นเวลานาน ใช่ นี่ไม่ใช่เป้าหมายในการเขียนบทความนี้ ข้อมูลทั้งหมดที่ให้ไว้ที่นี่เกี่ยวกับการทำงานของโปรเซสเซอร์ตระกูล x86 นั้นค่อนข้างผิวเผินและมีไว้เพื่อช่วยให้เข้าใจกลไกการทำงานของการเรียกระบบเคอร์เนลได้ดีขึ้นเล็กน้อย บางสิ่งสามารถเรียนรู้ได้โดยตรงจากโค้ดเคอร์เนล แม้ว่าเพื่อให้เข้าใจอย่างถ่องแท้ถึงสิ่งที่เกิดขึ้น ก็ยังแนะนำให้ทำความคุ้นเคยกับหลักการของโหมดที่ได้รับการป้องกัน ส่วนของโค้ดที่เริ่มต้น (แต่ไม่ได้ตั้งค่า!) IDT อยู่ใน arch/i386/kernel/head.S: /* * setup_idt * * ตั้งค่า idt ด้วย 256 รายการที่ชี้ไปที่ *ign_int ประตูขัดจังหวะ จริงๆ แล้วมันไม่ได้โหลด * idt - ซึ่งสามารถทำได้หลังจากเปิดใช้งานเพจแล้วเท่านั้น * และเคอร์เนลย้ายไปที่ PAGE_OFFSET การขัดจังหวะ * ถูกเปิดใช้งานที่อื่นเมื่อเราค่อนข้าง * แน่ใจว่าทุกอย่างโอเค * * คำเตือน: %esi ใช้งานผ่านฟังก์ชันนี้ */ 1.setup_idt: 2. lea ละเว้น_int,%edx 3. movl $(__KERNEL_CS<< 16),%eax 4. movw %dx,%ax /* selector = 0x0010 = cs */ 5. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 6. lea idt_table,%edi 7. mov $256,%ecx 8.rp_sidt: 9. movl %eax,(%edi) 10. movl %edx,4(%edi) 11. addl $8,%edi 12. dec %ecx 13. jne rp_sidt 14..macro set_early_handler handler,trapno 15. lea \handler,%edx 16. movl $(__KERNEL_CS << 16),%eax 17. movw %dx,%ax 18. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 19. lea idt_table,%edi 20. movl %eax,8*\trapno(%edi) 21. movl %edx,8*\trapno+4(%edi) 22..endm 23. set_early_handler handler=early_divide_err,trapno=0 24. set_early_handler handler=early_illegal_opcode,trapno=6 25. set_early_handler handler=early_protection_fault,trapno=13 26. set_early_handler handler=early_page_fault,trapno=14 28. ret หมายเหตุบางประการเกี่ยวกับโค้ด: โค้ดด้านบนเขียนในเวอร์ชันของแอสเซมเบลอร์ของ AT&T ดังนั้นความรู้เกี่ยวกับแอสเซมเบลอร์ในรูปแบบปกติของ Intel อาจทำให้เกิดความสับสนได้ ความแตกต่างที่สำคัญที่สุดคือลำดับของตัวถูกดำเนินการ หากลำดับที่กำหนดไว้สำหรับสัญลักษณ์ของ Intel คือ “ตัวสะสม”< "источник", то для ассемблера AT&T порядок прямой. Регистры процессора, как правило, должны иметь префикс "%", непосредственные значения (константы) префиксируются символом доллара "$". Синтаксис AT&T традиционно используется в Un*x-системах.

ในตัวอย่างข้างต้น บรรทัดที่ 2-4 ตั้งค่าที่อยู่เริ่มต้นของตัวจัดการขัดจังหวะทั้งหมด ตัวจัดการเริ่มต้นคือฟังก์ชัน Ign_int ซึ่งไม่ทำอะไรเลย การมีอยู่ของต้นขั้วนั้นเป็นสิ่งจำเป็นสำหรับการประมวลผลที่ถูกต้องของการขัดจังหวะทั้งหมดในขั้นตอนนี้ เนื่องจากยังไม่มีสิ่งอื่นใดเลย (อย่างไรก็ตาม มีการติดตั้งกับดักที่ต่ำกว่าเล็กน้อยในโค้ด - สำหรับกับดัก โปรดดูการอ้างอิงคู่มือสถาปัตยกรรม Intel หรืออะไรบางอย่าง คล้ายกันเราจะไม่พูดถึงพวกเขาที่นี่แตะกับดัก) บรรทัดที่ 5 กำหนดประเภทประตู ในบรรทัดที่ 6 เราโหลดการลงทะเบียนดัชนีพร้อมที่อยู่ของตาราง IDT ของเรา ตารางควรมี 255 ระเบียน แต่ละระเบียนมีขนาด 8 ไบต์ ในบรรทัดที่ 8-13 เราเติมทั้งตารางด้วยค่าเดียวกันที่ตั้งไว้ก่อนหน้าในรีจิสเตอร์ eax และ edx - นั่นคือนี่คือประตูขัดจังหวะที่อ้างอิงถึงตัวจัดการไม่สนใจ ด้านล่างเราจะกำหนดมาโครสำหรับการตั้งค่ากับดัก - บรรทัดที่ 14-22 ในบรรทัดที่ 23-26 โดยใช้มาโครด้านบน เราตั้งค่ากับดักสำหรับข้อยกเว้นต่อไปนี้: Early_divide_err - การหารด้วยศูนย์ (0), Early_illegal_opcode - คำสั่งตัวประมวลผลที่ไม่รู้จัก (6), Early_protection_fault - ความล้มเหลวในการป้องกันหน่วยความจำ (13), Early_page_fault - การแปลหน้า ความล้มเหลว (14) . จำนวน “การขัดจังหวะ” ที่เกิดขึ้นเมื่อสถานการณ์ผิดปกติที่เกี่ยวข้องเกิดขึ้นจะแสดงอยู่ในวงเล็บ ก่อนที่จะตรวจสอบประเภทโปรเซสเซอร์ใน arch/i386/kernel/head.S ตาราง IDT จะถูกตั้งค่าโดยการเรียก setup_idt: /* * เริ่มการตั้งค่าระบบแบบ 32 บิต เราจำเป็นต้องทำบางสิ่งที่ทำเสร็จแล้ว * ในโหมด 16 บิตอีกครั้งสำหรับการดำเนินการ "ของจริง"*/ 1. โทร setup_idt ... 2. โทร check_x87 3. lgdt Early_gdt_descr 4. lidt idt_descr

หลังจากค้นหาประเภทของโปรเซสเซอร์ (ร่วม) และดำเนินการตามขั้นตอนการเตรียมการทั้งหมดในบรรทัดที่ 3 และ 4 แล้ว เราจะโหลดตาราง GDT และ IDT ซึ่งจะใช้ในระหว่างขั้นตอนแรกของเคอร์เนล

จากการขัดจังหวะ กลับไปที่การโทรของระบบกัน ดังนั้นสิ่งที่จำเป็นในการให้บริการกระบวนการที่ร้องขอบริการบางอย่างคืออะไร? ในการเริ่มต้น คุณต้องย้ายจากวงแหวน 3 (ระดับสิทธิ์ CPL=3) ไปยังระดับสิทธิ์สูงสุด 0 (วงแหวน 0, CPL=0) เนื่องจาก รหัสเคอร์เนลอยู่ในส่วนที่มีสิทธิ์สูงสุด นอกจากนี้ จำเป็นต้องมีรหัสตัวจัดการที่จะให้บริการกระบวนการ นี่คือสิ่งที่เกตเวย์ 0x80 ใช้อย่างแน่นอน แม้ว่าจะมีการเรียกของระบบค่อนข้างน้อย แต่การเรียกทั้งหมดนั้นใช้จุดเข้าเดียว - int 0x80 ตัวจัดการถูกติดตั้งเมื่อเรียกใช้ฟังก์ชัน arch/i386/kernel/traps.c::trap_init(): เป็นโมฆะ __init trap_init(เป็นโมฆะ) ( ... set_system_gate(SYSCALL_VECTOR,&system_call); ... )เราสนใจบรรทัดนี้ใน trap_init() มากที่สุด ในไฟล์เดียวกันด้านบน คุณสามารถดูโค้ดสำหรับฟังก์ชัน set_system_gate() ได้: โมฆะคงที่ __init set_system_gate (int n ที่ไม่ได้ลงนาม, เป็นโมฆะ * addr) ( _set_gate (n, DESCTYPE_TRAP | DESCTYPE_DPL3, addr, __KERNEL_CS); )ที่นี่คุณจะเห็นได้ว่าประตูสำหรับการหยุดชะงัก 0x80 (กล่าวคือค่านี้ถูกกำหนดโดยมาโคร SYSCALL_VECTOR - คุณสามารถใช้คำพูดของฉันได้ :)) ได้รับการติดตั้งเป็นกับดักที่มีระดับสิทธิ์ DPL=3 (วงแหวน 3) เช่น การขัดจังหวะนี้จะถูกตรวจจับเมื่อถูกเรียกจากพื้นที่ผู้ใช้ ปัญหาเกี่ยวกับการเปลี่ยนจากวงแหวน 3 เป็นวงแหวน 0 เช่น แก้ไขแล้ว ฟังก์ชัน _set_gate() ถูกกำหนดไว้ในไฟล์ส่วนหัว include/asm-i386/desc.h สำหรับผู้ที่สงสัยเป็นพิเศษ มีโค้ดอยู่ด้านล่าง โดยไม่มีคำอธิบายยาวๆ: อินไลน์แบบคงที่เป็นโมฆะ _set_gate (ประตู int, ประเภท int ที่ไม่ได้ลงนาม, เป็นโมฆะ *addr, seg สั้นที่ไม่ได้ลงนาม) ( __u32 a, b; pack_gate(&a, &b, (ยาวที่ไม่ได้ลงนาม) addr, seg, ประเภท, 0); write_idt_entry (idt_table, gate , ก, ข)กลับไปที่ฟังก์ชัน trap_init() กัน มันถูกเรียกจากฟังก์ชัน start_kernel() ใน init/main.c หากคุณดูโค้ด trap_init() คุณจะเห็นว่าฟังก์ชันนี้เขียนค่าบางส่วนของตาราง IDT ใหม่อีกครั้ง - ตัวจัดการที่ใช้ในขั้นแรกของการเริ่มต้นเคอร์เนล (early_page_fault, Early_divide_err, Early_illegal_opcode, Early_protection_fault) จะถูกแทนที่ กับสิ่งที่จะใช้แล้วในระหว่างการทำงานของเคอร์เนลกระบวนการ ดังนั้นเราจึงเกือบจะถึงประเด็นแล้วและรู้อยู่แล้วว่าการเรียกของระบบทั้งหมดได้รับการประมวลผลเหมือนกัน - ผ่านเกตเวย์ int 0x80 ฟังก์ชัน system_call() ได้รับการติดตั้งเป็นตัวจัดการสำหรับ int 0x80 ดังที่เห็นได้จากโค้ดด้านบนนี้ arch/i386/kernel/traps.c::trap_init()

system_call()

รหัสสำหรับฟังก์ชัน system_call() อยู่ในไฟล์ arch/i386/kernel/entry.S และมีลักษณะดังนี้: # stub ตัวจัดการการโทรของระบบ ENTRY(system_call) RING0_INT_FRAME # ไม่สามารถคลายลงในพื้นที่ผู้ใช้ได้อีกต่อไป กด %eax # บันทึก orig_eax CFI_ADJUST_CFA_OFFSET 4 SAVE_ALL GET_THREAD_INFO(%ebp) # การติดตามการโทรของระบบในการดำเนินการ / การจำลอง /* หมายเหตุ _TIF_SECOMP คือหมายเลขบิต 8 และดังนั้นจึงจำเป็นต้องมี testw ไม่ใช่ testb */ testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp) jnz syscall_trace_entry cmpl $(nr_syscalls), %eax jae syscall_badsys syscall_call: call *sys_call_ table(,%eax , 4) movl %eax,PT_EAX(%esp) # เก็บค่าที่ส่งคืน ...รหัสไม่แสดงเต็ม อย่างที่คุณเห็น ขั้นแรก system_call() กำหนดค่าสแต็กให้ทำงานใน Ring 0, บันทึกค่าที่ส่งผ่านไปยังมันผ่าน eax ลงบนสแต็ก, บันทึกรีจิสเตอร์ทั้งหมดลงบนสแต็ก, รับข้อมูลเกี่ยวกับเธรดการเรียก และตรวจสอบว่าค่าที่ส่งผ่านหรือไม่ หมายเลขการเรียกของระบบ จะต้องไม่เกินขีดจำกัดของตารางการเรียกของระบบ และในที่สุด เมื่อใช้ค่าที่ส่งไปยัง eax เป็นอาร์กิวเมนต์ system_call() จะข้ามไปยังตัวจัดการเอาต์พุตของระบบจริงตามองค์ประกอบตารางที่อ้างอิงโดยดัชนีใน เอ๊กซ์ ตอนนี้จำตารางเวกเตอร์ขัดจังหวะเก่าที่ดีจากโหมดจริง ไม่เตือนคุณถึงอะไรเลยเหรอ? แน่นอนว่าในความเป็นจริงทุกอย่างค่อนข้างซับซ้อนกว่า โดยเฉพาะอย่างยิ่ง การเรียกของระบบจะต้องคัดลอกผลลัพธ์จากเคอร์เนลสแต็กไปยังสแต็กผู้ใช้ ส่งโค้ดส่งคืน และอื่นๆ ในกรณีที่อาร์กิวเมนต์ที่ระบุใน eax ไม่ได้อ้างอิงถึงการเรียกของระบบที่มีอยู่ (ค่าอยู่นอกช่วง) การเปลี่ยนไปใช้ป้ายกำกับ syscall_badsys จะเกิดขึ้น ที่นี่ ค่า -ENOSYS จะถูกผลักลงบนสแต็กที่ออฟเซ็ตที่ควรระบุค่า eax - การเรียกของระบบไม่ได้ถูกนำมาใช้ การดำเนินการของ system_call() เสร็จสมบูรณ์

ตารางการเรียกระบบอยู่ในไฟล์ arch/i386/kernel/syscall_table.S และมีรูปแบบที่ค่อนข้างง่าย: ENTRY(sys_call_table) .long sys_restart_syscall /* 0 - การเรียกของระบบ "setup()" แบบเก่า ซึ่งใช้สำหรับการรีสตาร์ท */ .long sys_exit .long sys_fork .long sys_read .long sys_write .long sys_open /* 5 */ .long sys_close .long sys_waitpid .ยาว sys_creat ...กล่าวอีกนัยหนึ่ง ทั้งตารางไม่มีอะไรมากไปกว่าอาร์เรย์ของที่อยู่ฟังก์ชันที่จัดเรียงตามลำดับหมายเลขการโทรของระบบที่ฟังก์ชันเหล่านี้ให้บริการ ตารางนี้เป็นอาร์เรย์ธรรมดาของคำที่ใช้เครื่องคู่ (หรือคำแบบ 32 บิต - ตามที่คุณต้องการ) รหัสสำหรับฟังก์ชันบางอย่างที่ให้บริการการเรียกระบบอยู่ในส่วนที่ขึ้นอยู่กับแพลตฟอร์ม - arch/i386/kernel/sys_i386.c และส่วนที่ไม่ขึ้นกับแพลตฟอร์ม - ใน kernel/sys.c

นี่เป็นกรณีที่มีการเรียกของระบบและเกต 0x80

กลไกใหม่สำหรับจัดการการเรียกของระบบใน Linux sysenter/sysexit.

ดังที่ได้กล่าวไปแล้ว เป็นที่ชัดเจนว่าการใช้วิธีการประมวลผลการเรียกระบบแบบดั้งเดิมที่ใช้เกต 0x80 จะทำให้ประสิทธิภาพลดลงบนโปรเซสเซอร์ Intel Pentium 4 ดังนั้น Linus Torvalds จึงนำกลไกใหม่ในเคอร์เนลโดยอิงจาก sysenter/sysexit คำแนะนำและออกแบบมาเพื่อเพิ่มประสิทธิภาพเคอร์เนลบนเครื่อง ซึ่งติดตั้งโปรเซสเซอร์ Pentium II และสูงกว่า (ด้วย Pentium II+ ที่โปรเซสเซอร์ Intel รองรับคำสั่ง sysenter/sysexit ดังกล่าว) สาระสำคัญของกลไกใหม่คืออะไร? ผิดปกติพอสมควร แต่สาระสำคัญยังคงเหมือนเดิม การดำเนินการมีการเปลี่ยนแปลง ตามเอกสารของ Intel คำสั่ง sysenter เป็นส่วนหนึ่งของกลไก "การเรียกระบบอย่างรวดเร็ว" โดยเฉพาะอย่างยิ่ง คำสั่งนี้ได้รับการปรับให้เหมาะสมเพื่อการย้ายจากระดับสิทธิพิเศษหนึ่งไปอีกระดับหนึ่งอย่างรวดเร็ว แม่นยำยิ่งขึ้นคือ เร่งความเร็วการเปลี่ยนเป็นวงแหวน 0 (วงแหวน 0, CPL=0) ในกรณีนี้ ระบบปฏิบัติการจะต้องเตรียมโปรเซสเซอร์เพื่อใช้คำสั่ง sysenter การตั้งค่านี้จะดำเนินการเพียงครั้งเดียวเมื่อโหลดและเริ่มต้นเคอร์เนลระบบปฏิบัติการ เมื่อเรียก sysenter จะตั้งค่าการลงทะเบียนตัวประมวลผลตามการลงทะเบียนเฉพาะเครื่องที่ระบบปฏิบัติการกำหนดไว้ก่อนหน้านี้ โดยเฉพาะอย่างยิ่ง การลงทะเบียนเซ็กเมนต์และการลงทะเบียนตัวชี้คำสั่ง - cs:eip รวมถึงเซ็กเมนต์สแต็กและตัวชี้บนสแต็ก - ss, esp ได้รับการติดตั้งแล้ว การเปลี่ยนไปใช้ส่วนของรหัสใหม่และการชดเชยจะดำเนินการจากวงแหวน 3 เป็น 0

คำสั่ง sysexit ทำสิ่งที่ตรงกันข้าม ทำให้การเปลี่ยนจากสิทธิ์ระดับ 0 เป็นสิทธิ์ระดับ 3 อย่างรวดเร็ว (CPL=3) ในกรณีนี้ การลงทะเบียนเซ็กเมนต์โค้ดจะถูกตั้งค่าเป็น 16 + ค่าเซกเมนต์ cs ที่จัดเก็บไว้ในการลงทะเบียนที่ขึ้นอยู่กับเครื่องของโปรเซสเซอร์ eip register ประกอบด้วยเนื้อหาของ edx register ผลรวมของ 24 และค่า cs ที่ป้อนโดย OS ก่อนหน้านี้ในการลงทะเบียนที่ขึ้นอยู่กับเครื่องของโปรเซสเซอร์เมื่อเตรียมบริบทสำหรับการทำงานของคำสั่ง sysenter จะถูกป้อนลงใน ss เนื้อหาของการลงทะเบียน ecx ถูกป้อนลงใน esp ค่าที่จำเป็นสำหรับการทำงานของคำสั่ง sysenter/sysexit จะถูกเก็บไว้ตามที่อยู่ต่อไปนี้:

  1. SYSENTER_CS_MSR 0x174 - รหัสเซ็กเมนต์โดยที่ค่าของเซ็กเมนต์ซึ่งมีรหัสตัวจัดการการเรียกของระบบถูกป้อน
  2. SYSENTER_ESP_MSR 0x175 - ตัวชี้ไปที่ด้านบนของสแต็กสำหรับตัวจัดการการโทรของระบบ
  3. SYSENTER_EIP_MSR 0x176 - ตัวชี้ไปยังออฟเซ็ตภายในส่วนของโค้ด ชี้ไปที่จุดเริ่มต้นของโค้ดตัวจัดการการเรียกของระบบ
ที่อยู่เหล่านี้อ้างอิงถึงรีจิสเตอร์ที่ขึ้นอยู่กับโมเดลที่ไม่มีชื่อ ค่าจะถูกเขียนไปยังรีจิสเตอร์ที่ขึ้นอยู่กับโมเดลโดยใช้คำสั่ง wrmsr ในขณะที่ edx:eax จะต้องมีส่วนนำหน้าและส่วนล่างของคำเครื่อง 64 บิต ตามลำดับ และ ecx จะต้องมีที่อยู่ของรีจิสเตอร์ที่รายการจะเข้าไป จะทำ ใน Linux ที่อยู่ของรีจิสเตอร์ที่ขึ้นอยู่กับโมเดลถูกกำหนดไว้ในไฟล์ส่วนหัว include/asm-i368/msr-index.h ดังต่อไปนี้ (ก่อนเวอร์ชัน 2.6.22 อย่างน้อยก็ถูกกำหนดไว้ในไฟล์ส่วนหัว include/asm-i386 /msr.h ฉันขอเตือนคุณว่าเราพิจารณากลไกการเรียกของระบบโดยใช้ตัวอย่างของเคอร์เนล Linux 2.6.22): #กำหนด MSR_IA32_SYSENTER_CS 0x00000174 #กำหนด MSR_IA32_SYSENTER_ESP 0x00000175 #กำหนด MSR_IA32_SYSENTER_EIP 0x00000176รหัสเคอร์เนลที่รับผิดชอบในการตั้งค่ารีจิสเตอร์ที่ขึ้นกับโมเดลจะอยู่ในไฟล์ arch/i386/sysenter.c และมีลักษณะดังนี้: 1. void Enable_sep_cpu(void) ( 2. int cpu = get_cpu(); 3. struct tss_struct *tss = &per_cpu(init_tss, cpu); 4. if (!boot_cpu_has(X86_FEATURE_SEP)) ( 5. put_cpu(); 6. 11. wrmsr (MSR_IA32_SYSENTER_EIP, (ยาวไม่ได้ลงนาม) sysenter_entry, 0);ที่นี่ ในตัวแปร tss เราได้รับที่อยู่ของโครงสร้างที่อธิบายส่วนของสถานะงาน TSS (Task State Segment) ใช้เพื่ออธิบายบริบทของงาน และเป็นส่วนหนึ่งของกลไกการทำงานหลายอย่างพร้อมกันของฮาร์ดแวร์สำหรับสถาปัตยกรรม x86 อย่างไรก็ตาม ในทางปฏิบัติแล้ว Linux ไม่ได้ใช้การสลับบริบทงานฮาร์ดแวร์ ตามเอกสารของ Intel การสลับไปทำงานอื่นสามารถทำได้โดยการดำเนินการคำสั่งข้ามส่วน (jmp หรือการโทร) ที่อ้างอิงถึงเซ็กเมนต์ TSS หรือโดยการดำเนินการตัวอธิบายประตูงานใน GDT (LDT) การลงทะเบียนโปรเซสเซอร์พิเศษซึ่งโปรแกรมเมอร์มองไม่เห็น - TR (Task Register) มีตัวเลือกตัวอธิบายงาน การโหลดรีจิสเตอร์นี้ยังโหลดฐานที่มองไม่เห็นด้วยซอฟต์แวร์และรีจิสเตอร์จำกัดที่เกี่ยวข้องกับ TR

แม้ว่า Linux จะไม่ใช้การสลับบริบทของฮาร์ดแวร์ แต่เคอร์เนลถูกบังคับให้จัดสรรรายการ TSS สำหรับโปรเซสเซอร์แต่ละตัวที่ติดตั้งบนระบบ เนื่องจากเมื่อโปรเซสเซอร์สลับจากโหมดผู้ใช้เป็นโหมดเคอร์เนล โปรเซสเซอร์จะดึงข้อมูลที่อยู่สแต็กเคอร์เนลจาก TSS นอกจากนี้ จำเป็นต้องใช้ TSS เพื่อควบคุมการเข้าถึงพอร์ต I/O TSS มีแผนที่ของสิทธิ์การเข้าถึงพอร์ต จากแผนที่นี้ คุณจะสามารถควบคุมการเข้าถึงพอร์ตสำหรับแต่ละกระบวนการโดยใช้คำสั่งเข้า/ออกได้ ที่นี่ tss->x86_tss.esp1 ชี้ไปที่เคอร์เนลสแต็ก __KERNEL_CS ชี้ไปที่ส่วนของรหัสเคอร์เนลโดยธรรมชาติ offset-eip คือที่อยู่ของฟังก์ชัน sysenter_entry()

ฟังก์ชัน sysenter_entry() ถูกกำหนดไว้ในไฟล์ arch/i386/kernel/entry.S และมีลักษณะดังนี้: /* SYSENTER_RETURN ชี้ไปที่หลังคำสั่ง "sysenter" ในหน้า vsyscall ดูvsyscall-sysentry.Sซึ่งกำหนดสัญลักษณ์ */ # sysenter ตัวจัดการการโทร stub ENTRY(sysenter_entry) CFI_STARTPROC แบบง่าย CFI_SIGNAL_FRAME CFI_DEF_CFA esp, 0 CFI_REGISTER esp, ebp movl TSS_sysenter_esp0(%esp),%esp sysenter_past_esp: /* * ไม่จำเป็นต้องปฏิบัติตามส่วนเปิด/ปิด irqs นี้: syscall * ปิดการใช้งาน irqs และที่นี่เราเปิดใช้งานโดยตรงหลังจากรายการ: */ ENABLE_INTERRUPTS(CLBR_NONE) pushl $(__USER_DS) CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET ss, 0*/ pushl %ebp CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET esp, 0 pushfl CFI_ADJUST_ CFA_OFFSET 4 ลิตร $(__USER_CS) CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET cs, 0*/ /* * กด current_thread_info()->sysenter_return ไปที่สแต็กเช่นเดียวกับ system_call() งานส่วนใหญ่จะเสร็จสิ้นในการเรียกสาย *sys_call_table(,%eax,4) นี่คือที่ที่เรียกตัวจัดการการโทรของระบบเฉพาะ เห็นได้ชัดว่ามีการเปลี่ยนแปลงเล็กน้อยโดยพื้นฐาน ความจริงที่ว่าตอนนี้อินเทอร์รัปต์เวกเตอร์ถูกฝังอยู่ในฮาร์ดแวร์และโปรเซสเซอร์ช่วยให้เราย้ายจากระดับสิทธิพิเศษหนึ่งไปยังอีกระดับหนึ่งได้เร็วขึ้น การเปลี่ยนแปลงรายละเอียดการดำเนินการบางส่วนที่มีเนื้อหาเดียวกันเท่านั้น จริงอยู่ที่การเปลี่ยนแปลงไม่ได้จบเพียงแค่นั้น จำได้ว่าเรื่องราวเริ่มต้นอย่างไร ในตอนแรกฉันได้กล่าวถึงวัตถุที่แบ่งปันเสมือนแล้ว ดังนั้น หากก่อนหน้านี้การนำการเรียกระบบไปใช้ เช่น จากไลบรารีระบบ libc ดูเหมือนเป็นการเรียกขัดจังหวะ (แม้ว่าไลบรารีจะใช้ฟังก์ชันบางอย่างเพื่อลดจำนวนสวิตช์บริบท) ตอนนี้ต้องขอบคุณ VDSO ที่ทำให้การเรียกของระบบ สามารถทำได้เกือบโดยตรง โดยไม่ต้องใช้ libc มันอาจจะถูกนำมาใช้โดยตรงมาก่อนอีกครั้งเป็นการหยุดชะงัก แต่ตอนนี้สามารถร้องขอการโทรเป็นฟังก์ชันปกติที่ส่งออกจากไลบรารีที่เชื่อมโยงแบบไดนามิก (DSO) เมื่อบู๊ตเครื่อง เคอร์เนลจะกำหนดกลไกที่ควรและสามารถใช้ได้สำหรับแพลตฟอร์มที่กำหนด เคอร์เนลจะตั้งค่าจุดเริ่มต้นให้กับฟังก์ชันที่ดำเนินการเรียกระบบ ทั้งนี้ขึ้นอยู่กับสถานการณ์ ถัดไป ฟังก์ชันจะถูกส่งออกไปยังพื้นที่ผู้ใช้เป็นไลบรารี linux-gate.so.1 ไลบรารี linux-gate.so.1 ไม่มีอยู่ในดิสก์ พูดง่ายๆ ก็คือ จำลองโดยเคอร์เนลและมีอยู่ตราบเท่าที่ระบบกำลังทำงานอยู่ หากคุณหยุดระบบและเมาต์ระบบไฟล์รูทจากระบบอื่น คุณจะไม่พบไฟล์นี้บนระบบไฟล์รูทของระบบที่หยุดทำงาน ที่จริงแล้ว คุณจะไม่สามารถค้นหามันได้แม้แต่ในระบบที่ทำงานอยู่ก็ตาม ในทางกายภาพมันก็ไม่มีอยู่จริง นี่คือสาเหตุที่ linux-gate.so.1 เป็นอย่างอื่นที่ไม่ใช่ VDSO - เช่น วัตถุที่ใช้ร่วมกันแบบไดนามิกเสมือน เคอร์เนลแมปไลบรารีไดนามิกจึงจำลองลงในพื้นที่ที่อยู่ของแต่ละกระบวนการ คุณสามารถตรวจสอบสิ่งนี้ได้อย่างง่ายดายด้วยการรันคำสั่งต่อไปนี้: f0x@devel0:~$ cat /proc/self/maps 08048000-0804c000 r-xp 00000000 08:01 46 /bin/cat 0804c000-0804d000 rw-p 00003000 08:01 46 /bin/cat 0804d000-0806 e0 00 rw-p 0804d000 00:00 0 ... b7fdf000-b7fe1000 rw-p 00019000 08:01 2066 /lib/ld-2.5.so bffd2000-bffe8000 rw-p bffd2000 00:00 0 ffffe000-ffffff000 r-xp 00000000 00 :00 0บรรทัดสุดท้ายคือวัตถุที่เราสนใจ: ffffe000-fffff000 r-xp 00000000 00:00 0จากตัวอย่างข้างต้น เห็นได้ชัดว่าออบเจ็กต์กินพื้นที่หนึ่งหน้าในหน่วยความจำ - 4,096 ไบต์ เกือบจะอยู่ที่ด้านหลังของพื้นที่ที่อยู่ เรามาทำการทดลองอีกครั้ง: f0x@devel0:~$ ldd `ซึ่ง cat` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e87000) /lib/ ld-linux.so.2 (0xb7fdf000) f0x@devel0:~$ ldd `ซึ่ง gcc` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc .so.6 (0xb7e3c000) /lib/ld-linux.so.2 (0xb7f94000) f0x@devel0:~$ที่นี่เราเพิ่งเอาสองแอปพลิเคชันออกไปทันที จะเห็นได้ว่าไลบรารีถูกแมปลงในพื้นที่ที่อยู่ของกระบวนการที่มีที่อยู่ถาวรเดียวกัน - 0xffffe000 ทีนี้เรามาดูกันว่าจริง ๆ แล้วมีอะไรเก็บไว้ในหน้าหน่วยความจำนี้บ้าง...

คุณสามารถดัมพ์เพจหน่วยความจำที่เก็บโค้ดที่ใช้ร่วมกันของ VDSO ได้โดยใช้โปรแกรมต่อไปนี้: #include #include #include int main () ( char* vdso = 0xffffe000; char* buffer; FILE* f; buffer = malloc (4096); if (!buffer) exit (1); memcpy (บัฟเฟอร์, vdso, 4096) ; if (!(f = fopen("test.dump", "w+b"))) ( free (buffer); exit (1); ) fwrite (บัฟเฟอร์, 4096, 1, f); ); กลับ 0;พูดอย่างเคร่งครัด ก่อนหน้านี้สามารถทำได้ง่ายกว่าโดยใช้คำสั่ง dd if=/proc/self/mem of=test.dump bs=4096 ข้าม=1048574 นับ=1แต่เคอร์เนลตั้งแต่เวอร์ชัน 2.6.22 หรืออาจจะก่อนหน้านั้นด้วยซ้ำ จะไม่แมปหน่วยความจำกระบวนการกับ /proc/`pid`/mem อีกต่อไป เห็นได้ชัดว่าไฟล์นี้ได้รับการบันทึกไว้เพื่อความเข้ากันได้ แต่ไม่มีข้อมูลเพิ่มเติม

มาคอมไพล์และรันโปรแกรมที่กำหนดกัน ลองแยกส่วนโค้ดผลลัพธ์: f0x@devel0:~/tmp$ objdump --disassemble ./test.dump ./test.dump: รูปแบบไฟล์ elf32-i386 การแยกส่วน .text: ffffe400<__kernel_vsyscall>: ffffe400: 51 กด %ecx ffffe401: 52 กด %edx ffffe402: 55 กด %ebp ffffe403: 89 e5 mov %esp,%ebp ffffe405: 0f 34 sysenter ... ffffe40e: eb f3 jmp ffffe403<__kernel_vsyscall+0x3>ffffe410: 5d ป๊อป %ebp ffffe411: 5a ป๊อป %edx ffffe412: 59 ป๊อป %ecx ffffe413: c3 ret ... f0x@devel0:~/tmp$นี่คือเกตเวย์ของเราสำหรับการเรียกของระบบ ทั้งหมดนี้อยู่ในมุมมองแบบเต็ม กระบวนการ (หรือไลบรารีระบบ libc) ที่เรียกใช้ฟังก์ชัน __kernel_vsyscall จะจบลงที่ที่อยู่ 0xffffe400 (ในกรณีของเรา) ถัดไป __kernel_vsyscall จะบันทึกเนื้อหาของ ecx, edx, ebp register บนสแต็กกระบวนการของผู้ใช้ เราได้พูดคุยเกี่ยวกับวัตถุประสงค์ของการลงทะเบียน ecx และ edx ก่อนหน้านี้แล้ว ใน ebp จะใช้ในภายหลังเพื่อกู้คืนสแต็กของผู้ใช้ คำสั่ง sysenter จะดำเนินการ "การสกัดกั้นการขัดจังหวะ" และผลที่ตามมาคือการเปลี่ยนไปใช้ sysenter_entry ครั้งต่อไป (ดูด้านบน) คำสั่ง jmp ที่ 0xffffe40e ถูกแทรกเพื่อรีสตาร์ทการเรียกระบบด้วย 6 อาร์กิวเมนต์ (ดู http://lkml.org/lkml/2002/12/18/) โค้ดที่วางบนเพจอยู่ในไฟล์ arch/i386/kernel/vsyscall-enter.S (หรือ arch/i386/kernel/vsyscall-int80.S สำหรับ hook 0x80) แม้ว่าฉันจะพบว่าที่อยู่ของฟังก์ชัน __kernel_vsyscall นั้นคงที่ แต่ก็มีความเห็นว่าไม่เป็นเช่นนั้น โดยทั่วไป ตำแหน่งของจุดเริ่มต้นใน __kernel_vsyscall() สามารถพบได้จากเวกเตอร์ ELF-auxv โดยใช้พารามิเตอร์ AT_SYSINFO เวกเตอร์ ELF-auxv มีข้อมูลที่ส่งผ่านไปยังกระบวนการผ่านสแต็กเมื่อเริ่มต้นระบบ และมีข้อมูลต่างๆ ที่จำเป็นในขณะที่โปรแกรมกำลังทำงาน เวกเตอร์นี้มีตัวแปรสภาพแวดล้อมกระบวนการ อาร์กิวเมนต์ ฯลฯ โดยเฉพาะ

นี่คือตัวอย่างเล็กๆ ใน C ของวิธีที่คุณสามารถเรียกใช้ฟังก์ชัน __kernel_vsyscall ได้โดยตรง: #รวม อินท์พิด; int main () ( __asm ​​​​("movl $20, %eax \n" "call *%gs:0x10 \n" "movl %eax, pid \n"); printf ("pid: %d\n", พีไอดี) ; ส่งกลับ 0;ตัวอย่างนี้นำมาจากเพจ Manu Garg, http://www.manugarg.com ดังนั้น ในตัวอย่างข้างต้น เราทำการเรียกระบบ getpid() (หมายเลข 20 หรืออย่างอื่น __NR_getpid) เพื่อไม่ให้ปีนขึ้นไปบนสแต็กกระบวนการเพื่อค้นหาตัวแปร AT_SYSINFO เราจะใช้ประโยชน์จากข้อเท็จจริงที่ว่าไลบรารีระบบ libc.so คัดลอกค่าของตัวแปร AT_SYSINFO ไปยัง Thread Control Block (TCB) เมื่อโหลด โดยทั่วไปบล็อกข้อมูลนี้จะถูกอ้างอิงโดยตัวเลือกใน gs เราถือว่าพารามิเตอร์ที่ต้องการอยู่ที่ออฟเซ็ต 0x10 และทำการเรียกไปยังที่อยู่ที่เก็บไว้ใน %gs:$0x10

ผลลัพธ์.

ในความเป็นจริง ในทางปฏิบัติ ไม่สามารถเพิ่มประสิทธิภาพอย่างมีนัยสำคัญได้เสมอไป แม้ว่าจะรองรับ FSCF (Fast System Call Facility) บนแพลตฟอร์มนี้ก็ตาม ปัญหาคือว่ากระบวนการนี้ไม่ค่อยเข้าถึงเคอร์เนลโดยตรงไม่ทางใดก็ทางหนึ่ง และมีเหตุผลที่ดีสำหรับเรื่องนี้ การใช้ไลบรารี libc ช่วยให้คุณรับประกันความสามารถในการพกพาของโปรแกรมโดยไม่คำนึงถึงเวอร์ชันของเคอร์เนล และผ่านไลบรารีระบบมาตรฐานที่การเรียกระบบส่วนใหญ่ดำเนินการ แม้ว่าคุณจะคอมไพล์และติดตั้งเคอร์เนลล่าสุดที่คอมไพล์สำหรับแพลตฟอร์มที่รองรับ FSCF แต่ก็ไม่ได้รับประกันประสิทธิภาพที่เพิ่มขึ้น ความจริงก็คือไลบรารีระบบของคุณ libc.so จะยังคงใช้ int 0x80 ต่อไป และคุณสามารถจัดการกับสิ่งนี้ได้โดยการสร้าง glibc ใหม่เท่านั้น ไม่ว่าอินเทอร์เฟซ VDSO และ __kernel_vsyscall โดยทั่วไปจะรองรับ glibc หรือไม่ ฉันพบว่ามันยากที่จะตอบในขณะนี้

ลิงค์

เพจมนูการ์ก http://www.manugarg.com
กระจาย/รวบรวมความคิดโดย Johan Petersson, http://www.trilithium.com/johan/2005/08/linux-gate/
เก่าดี การทำความเข้าใจเคอร์เนล Linux เราจะอยู่ที่ไหนถ้าไม่มีมัน :)
และแน่นอนว่าซอร์สโค้ด Linux (2.6.22)