برنامه چجور با Kernel حرف میزنه؟
تا اینجا با چیزهایی مثل:
lua
fork()
exec()
wait()
exit()
کار کردیم
اما یک سوال مهم وجود داره
این توابع چطور میتونن از Kernel درخواست انجام یک کار بکنن؟
مثلا برنامهای که داخل User Space اجرا میشه چطور میتونه:
فایل باز کنه؟
داده بخوانه؟
Process
جدید بسازه؟
حافظه بگیره؟
Process
خودش رو تموم کنه؟
جواب اصلی:
System Call
User Mode و Kernel Mode
CPU
معمولا اجرای برنامه رو در سطح دسترسی محدودی انجام میدهد که به اون:
User Mode
میگیم
برنامه در این حالت نمیتونه مستقیما هر کاری که دلش خواست با سیستم انجام بده
مثلا نمیتونه مستقیما:
Hardware
Kernel Memory
Page Tables
Device Control
رو دستکاری کنه
چون اگر هر برنامهای چنین دسترسیای داشت یک برنامه خرابکار میتونست کل سیستم رو به فنا بده😁
و ادم ها قبلا به اندازه کافی راه های دیگه ای برای این کار پیدا کردن
Kernel Mode
Kernel
در سطح دسترسی بالاتری اجرا میشه:
Kernel Mode
در این حالت Kernel میتونه به منابع حساس سیستم دسترسی داشته باشه
پس ساختار کلی تقریبا این شکلیه:
User Space
│
│ System Call
↓
Kernel Space
│
↓
Hardware / Resources
برنامه مستقیم وارد Kernel نمیشه
بلکه از یک دروازه کنترل شده استفاده میکنه این دروازه همون System Call هست
یک مثال ساده
فرض کنیم برنامه میخاد از یک فایل بخونه
در سطح برنامه ممکنه چیزی شبیه این داشته باشیم:
read(fd, buffer, size);
برنامه درخواست میکنه:
من میخاهم از این File Descriptor
به اندازه مشخصی داده بخونم
در اخر درخواست باید به Kernel برسه
Kernel
بررسی میکنه:
این Process چه کسیه؟
اجازه دسترسی داره؟
fd
معتبره؟
آدرس buffer معتبره؟
چه مقدار داده باید خونده بشه؟
بعد عملیات رو انجام میده و نتیجه رو برمیگردونه
مسیر کلی System Call
به شکل ساده:
Program
│
↓
Library / API Wrapper
│
↓
System Call Mechanism
│
↓
CPU → Kernel Mode
│
↓
Kernel
│
↓
Operation
│
↓
Return Value
│
↓
User Mode
یعنی برنامه برای انجام عملیات حساس از یک مسیر مشخص وارد Kernel میشه
System Call با Function Call
یکی نیست
این دو تا رو قاطی نکنیم
یک Function Call معمولی مثلا:
result = add(a, b);
معمولا در همون فضای اجرای برنامه انجام میشه
اما System Call یک عبور از مرز دسترسی User و Kernel ایجاد میکنه
پس:
Function Call
↓
همون فضای اجرا
System Call
↓
User Mode
↓
Kernel Mode
↓
برگشت به User Mode
این تفاوت در Reverse Engineering خیلی مهمه
fork() و wait() هم از همین مسیر استفاده میکنن
مثلا در برنامه C مینویسیم:
fork();
اما این به معنی یک Function ساده مثل:
C
int add(int a, int b)
نیست
در سیستمهای Unix-like این درخواست در اخر باید به Kernel برسه تا Kernel بتونه Process جدید ایجاد کنه
همین ایده برای عملیات هایی مثل:
shell
read
write
open
mmap
fork
exec
wait
هم وجود داره
البته یک نکته مهم
چیزی که در کد میبینیم لزوما خود System Call خام نیست ممکنه یک Library Wrapper باشه که در اخر System Call مناسب رو انجام میده
System Call
چطور به Kernel میرسه؟
در معماریهای مدرن CPU مکانیزم مخصوصی برای این انتقال داره
مثلا در x86-64 لینوکس دستور:
syscall
برای ورود به مسیر System Call استفاده میشه
به صورت مفهومی:
User Code
↓
Arguments آماده میشن
↓
System Call Number
↓
syscall
↓
CPU
↓
Kernel Entry
↓
Kernel Handler
Kernel
بعد از ورود درخواست رو بررسی و پردازش میکنه
برای ما چرا مهمه؟
فرض کنید داخل یک Binary این رو ببینید:
syscall
دیگه فقط با یک دستور اسمبلی معمولی طرف نیستید
این دستور میتونه نشون دهنده عبور برنامه از:
User Space
↓
Kernel Space
باشه
از طرف دیگه اگر داخل کد برنامه API هایی مثل:
CreateFile
ReadFile
VirtualAlloc
CreateProcess
`رو ببینید باید بدونید این API ها در اخر برای انجام تعداد زیادی از عملیات های سیستمی به مکانیزمهای Kernel وابسته هستن پس در RE باید بتونید این زنجیره رو در ذهنتون ببینید:
Application
↓
Library / API
↓
System Call
↓
Kernel
↓
Resource / Hardware
این دیدگاه بعدا هنگام تحلیل Process ها Memory File System Thread ها و حتی رفتارهای پیچیدهتر Binary ها خیلی به کارتون میاد