یہاں ایک چھوٹا سی پروگرام ہے۔ یہ کال کرتا ہے clock_gettime() اس کے بعد یہ معیاری آؤٹ پٹ پر 5 بائٹس لکھتا ہے۔
#include
#include
#include
int main(void)
{
struct timespec ts;
for (int i = 0; i < 3; i++)
clock_gettime(CLOCK_MONOTONIC, &ts);
write(1, "donen", 5);
return 0;
}
دونوں سسٹم کالز کی طرح نظر آتے ہیں۔ دونوں دانا سے ایسی چیز مانگتے ہیں جو پروگرام خود حاصل نہیں کرسکتا: موجودہ وقت اور فائل ڈسکرپٹرز تک رسائی۔
اب اسے نیچے چلائیں۔ straceعمل کے ذریعے کی گئی تمام سسٹم کالز کی رپورٹ کرتا ہے۔
gcc -O0 -o mystery mystery.c
strace ./mystery 2>&1 | grep -c clock_gettime
جواب ہے 0.
کہ write() یہ فوری طور پر ظاہر ہوتا ہے۔ تین چیزیں clock_gettime() مجھے کوئی کال نظر نہیں آرہی ہے۔ وہی پروگرام، وہی libc، وہی سسٹم، ان میں سے کوئی بھی دانا تک نہیں پہنچتا۔
اس مضمون کے اختتام تک آپ اپنے کاموں کے درمیان تمام مراحل کو جان لیں گے۔ write() اور دانا کے اندر کون سا کوڈ چلتا ہے، واپسی کا راستہ اندر جانے والے راستے سے اجنبی کیوں ہے، اور کیوں؟ clock_gettime() آپ پوری چیز کو چھوڑ رہے ہوں گے۔
انڈیکس
آپ کو کیا ضرورت ہے
لینکس چلانے والے x86-64 سسٹم کی ضرورت ہے۔ gcc, straceاور objdump. Debian یا Ubuntu پر build-essential, straceاور binutils. آپ کو C پڑھنے میں بھی ماہر ہونا چاہیے۔ آپ کو کوئی کرنل کوڈ لکھنے کی ضرورت نہیں ہے، اور ہم یہاں کرنل کی تعمیر یا انسٹال نہیں کرتے ہیں۔
نیچے دی گئی ہر چیز ایک عام صارف اکاؤنٹ کے تحت چلتی ہے سوائے ایک اختیاری ٹریکنگ قدم کے جس کی ضرورت ہے۔ sudo.
دائرہ کار کے بارے میں دو انتباہات۔ سب سے پہلے، اس مضمون کا مواد ہے صرف x86-64. ARM64 مختلف ہدایات اور رجسٹر کے مختلف قوانین کے ساتھ ایک ہی کام کرتا ہے، اور دونوں کے لیے ہر بیان کو ہیج کرنے سے لمبائی دوگنی ہوجاتی ہے اور وضاحت آدھی ہوجاتی ہے۔ دوسرا، دانا کے اندر کی حرکت ہوتی ہے۔ میں نے یہاں سب کچھ کیا ہے۔ Linux 5.15 (Ubuntu 22.04, Intel Core i7-10750H)میں ان جگہوں پر جھنڈا لگاؤں گا جہاں نئے دانا مختلف ہیں۔ اپنا ورژن چیک کریں۔ uname -r.
صارف کی جگہ سے سسٹم کال کیسا لگتا ہے۔
آئیے اہم حل کے ساتھ شروع کریں۔ write() یہ سسٹم کال نہیں ہے۔ یہ سی لائبریری کا ایک عام سی فنکشن ہے۔ وہ فنکشن آپ کی طرف سے سسٹم کالز انجام دیتا ہے، اور دونوں کے درمیان فرق یہ ہے کہ دانا کے بارے میں سب سے زیادہ الجھن شروع ہوتی ہے۔
آپ اسے libc کاٹ کر اور اسے براہ راست کال کر کے ثابت کر سکتے ہیں۔
x86-64 پر، سسٹم کالز کے مقررہ اصول ہوتے ہیں۔ بس وہ فون نمبر درج کریں جو آپ چاہتے ہیں۔ raxاور دلیل rdi, rsi, rdx, r10, r8اور r9اس ترتیب میں۔ پھر اس طرح ایک ہی کمانڈ چلائیں: syscall.
نمبر وہ نہیں ہیں جو آپ کو یاد ہیں۔ وہ آپ کے کمپیوٹر کے ہیڈرز میں موجود ہیں:
grep -E "__NR_(write|getpid|clock_gettime) " /usr/include/x86_64-linux-gnu/asm/unistd_64.h
اس مشین پر:
#define __NR_write 1
#define __NR_getpid 39
#define __NR_clock_gettime 228
تو write کال نمبر 1۔ یہاں ایک کال ہے جو براہ راست libc ریپر استعمال کیے بغیر لکھی گئی ہے۔
static long raw_write(int fd, const void *buf, unsigned long count)
{
long ret;
__asm__ volatile (
"syscall"
: "=a" (ret) /* the result comes back in rax */
: "a" (1L), /* rax = 1, the syscall number for write */
"D" ((long)fd), /* rdi = first argument */
"S" (buf), /* rsi = second argument */
"d" (count) /* rdx = third argument */
: "rcx", "r11", "memory"
);
return ret;
}
اگر آپ اسے مرتب کرتے ہیں اور چلاتے ہیں تو، بائٹس آپ کے راستے میں libc کے بغیر معیاری آؤٹ پٹ پر ظاہر ہوں گی۔
آخری لائن دیکھیں، سہ شاخہ کی فہرست۔ یہ مرتب کرنے والے کو بتاتا ہے۔ rcx اور r11 تباہ ہو جائے گا۔ حفاظتی وجوہات کی بناء پر شامل نہیں کیا گیا۔ یہ ہارڈ ویئر کے بارے میں ایک حقیقت ہے اور خاموشی سے مندرجہ بالا قواعد کے بارے میں کچھ عجیب و غریب چیزوں کی وضاحت کرتا ہے۔
x86-64 پر C فنکشنز چوتھی دلیل لیتے ہیں۔ rcx. سسٹم کال سسٹم کال کو پاس کرتی ہے۔ r10 اس کے بجائے. کوئی بھی وضاحت جو کہتی ہے کہ "کیونکہ یہ رواج ہے” بہت جلد رک جاتا ہے۔ اصل وجہ یہ ہے۔ syscall اوور رائٹ کمانڈ rcx اپنے فرائض کی انجام دہی کے حصے کے طور پر۔ دانا چاہنے کے باوجود بھی چوتھی دلیل کو قبول نہیں کر سکتا تھا، لہذا ABI کو اس کے اپنے ہارڈ ویئر کے گرد گھیر لیا گیا۔
یہ پہلی علامت ہے کہ یہ باؤنڈری کاسٹیومڈ فنکشن کال نہیں ہے۔ مختلف میکانزم، مختلف قواعد، اور ہارڈ ویئر پہلے وہاں پہنچے۔
آپ مرتب شدہ بائنری میں خود ہدایات دیکھ سکتے ہیں۔
objdump -d --no-show-raw-insn raw_write | grep -B2 -A2 syscall
ہدایات وہیں ہیں:
118b: mov -0x28(%rbp),%rdx
118f: syscall
1191: mov %rax,-0x8(%rbp)
تین لائنیں: ایک رجسٹر لوڈ کریں، ایک ہدایات پر عمل کریں، اور جو واپس آتا ہے اسے محفوظ کریں۔ اس مضمون میں باقی سب کچھ دوسری اور تیسری لائنوں کے درمیان ہوتا ہے۔
کراسنگ
جب سی پی یو چلتا ہے۔ syscallیہ وہی کرتا ہے جو باقاعدہ چھلانگ نہیں لگا سکتا۔ یعنی یہ پروسیسر کے استحقاق کی سطح کو تبدیل کرتا ہے۔ آپ کا کوڈ اس میں چلتا ہے جسے x86 رنگ 3 کہتے ہیں۔ کرنل کوڈ رنگ 0 میں چلتا ہے۔
کمانڈ ترتیب میں تین کام انجام دیتا ہے:
-
پروگرام کی اگلی ہدایات کا پتہ درج ذیل جگہ پر محفوظ کریں۔
rcx. واپسی کا پتہ یہی ہے، اسی لیےrcxیہ کچل دیا گیا ہے۔ -
CPU جھنڈوں کو درج ذیل جگہ پر اسٹور کریں:
r11. -
یہ تین خصوصی CPU رجسٹروں سے نئے انسٹرکشن پوائنٹرز، کوڈ سیگمنٹس، اور اسٹیک سیگمنٹس کو لوڈ کرتا ہے۔
تیسرا مرحلہ اہم ہے۔ نیا انسٹرکشن پوائنٹر پروگرام سے نہیں آتا ہے۔ یہ مندرجہ ذیل مشین کے مخصوص رجسٹروں سے آتا ہے: LSTARاور دانا نے وہ رجسٹر بوٹ کے دوران لکھے۔.
یہ حفاظتی خاصیت ہے جو پورے ڈیزائن کو زیر کرتی ہے۔ صارف کی جگہ منتقلی کو متحرک کرتی ہے۔ صارف کی جگہ کو یہ منتخب کرنے کے لئے نہیں ملتا ہے کہ وہ کہاں اترتا ہے۔ ایک دروازہ ہے، دانا اسے انسٹال کرتا ہے اور اگلے دروازے سے کھلتا ہے۔ entry_SYSCALL_64 کو arch/x86/entry/entry_64.S.
حکم کیا کرتا ہے اس پر توجہ دیں۔ ~ نہیں کرو یہ انٹرپٹ ڈسکرپٹر ٹیبل یا پش استثنائی فریموں کا حوالہ نہیں دیتا ہے۔ پرانے نظام بذریعہ کرنل تک پہنچے: int 0x80جڑی ہوئی ہر مشین کے ساتھ سافٹ ویئر میں رکاوٹ کی وجہ سے یہ سست تھا۔ کہ syscall رہنما خطوط موجود ہیں کیونکہ یہ راستہ سرشار ہارڈویئر بنانے کے قابل تھا۔
دانا بن جاؤ
منزل entry_SYSCALL_64 ایسا نہیں ہے کہ کرنل کوڈ چلنے کے لیے تیار ہے۔ آمد کے وقت CPU رنگ 0 میں ہے لیکن پھر بھی مصروف ہے۔ آپ کا اسٹیک اور آپ کا رجسٹر کی حیثیت۔ دانا کو اسے درست کرنے کی ضرورت ہے اس سے پہلے کہ وہ محفوظ طریقے سے آپریشن کر سکے۔
تین چیزیں ہوتی ہیں، اور یہ تینوں پہلے سے چلنے والے نظام میں اعتماد قائم کرنے والے دانا ہیں۔
1۔ swapgs
دانا فی سی پی یو پوائنٹر برقرار رکھتا ہے۔ GS آپ اپنے ڈیٹا ڈھانچے کو رجسٹر کرکے تلاش کرسکتے ہیں۔ جب آپ دوڑ رہے ہیں، GS اس میں وہ سب کچھ ہوتا ہے جو آپ کا پروگرام وہاں رکھتا ہے۔ واحد حکم، swapgsکرنل ویلیو کے لیے اس کا تبادلہ کریں۔ اس کے بارے میں دانا کی دستاویزات غیر معمولی طور پر دو ٹوک ہیں، اسے کمزور قرار دیتے ہیں اور انتباہ کرتے ہیں کہ اسے بالکل نیسٹڈ ہونا چاہیے۔ اسے کسی بھی سمت میں غلط سمجھیں اور آگے آپ کو بہت بری دوپہر گزرے گی۔
2. اسٹیک سوئچ
اسٹیک پوائنٹر ایک پروگرام کے ذریعہ منتخب کردہ قدر ہے، لہذا دانا اس پر بھروسہ نہیں کرسکتا۔ یہ تمہارا ہے rsp اس کے بعد یہ اس تھریڈ کو تفویض کردہ کرنل اسٹیک پر سوئچ کرتا ہے۔
3. عمارت pt_regs
دانا پھر محفوظ شدہ رجسٹروں کو ایک مخصوص ترتیب میں ایک نئے اسٹیک پر دھکیلتا ہے، اس طرح ایک C ڈھانچہ بناتا ہے: struct pt_regs. pt_regs آپ کا عمل منجمد ہے۔ ہر ڈیبگر جو رکے ہوئے عمل کی جانچ کرتا ہے، ہر سگنل ہینڈلر جو واپس کیے گئے سیاق و سباق میں ترمیم کرتا ہے، اور ہر سسٹم کال ہینڈلر اس ڈھانچے سے اپنے دلائل پڑھتا ہے۔
چوتھا مرحلہ ہو سکتا ہے۔ اگر CPU پگھلنے کے لیے حساس ہے، تو دانا یہاں صفحہ کی میزیں بھی تبدیل کرتا ہے۔ اس کی وجہ یہ ہے کہ وہ چپس کرنل میموری کو محفوظ طریقے سے میپ نہیں رکھ سکتی ہیں جب کوڈ چل رہا ہے۔
تبادلہ مفت نہیں ہے۔ یہی وجہ ہے کہ 2018 میں سسٹم کالز نمایاں طور پر سست تھیں، اور اس مضمون میں بعد میں کچھ نمبر تین سال پرانے سسٹمز پر مختلف کیوں نظر آتے ہیں۔
آپ یہ جان سکتے ہیں کہ آیا آپ کی مشین خود ادائیگی کرتی ہے۔
cat /sys/devices/system/cpu/vulnerabilities/meltdown
یہاں ٹیسٹ مشین کی رپورٹ ہے: Not affectedسلیکون کی تخلیق اس وجہ سے ہے کہ ہارڈ ویئر میں تبدیلیاں کی گئی ہیں۔ ایک پرانا لیپ ٹاپ رپورٹ کرتا ہے۔ Mitigation: PTIاور ہر سسٹم جس کو کال کرتا ہے بالکل اسی مقام پر اضافی کام کرتا ہے۔
جب آپ وہاں ہوں تو اس ڈائرکٹری میں موجود دیگر فائلوں پر ایک نظر ڈالیں۔ ہر ایک ایک ریلیف ہے جس کی قیمت یہ سرحد ادا کر سکتی ہے۔
دانا کے اندر: ہینڈلر کی تلاش
دانا اب اپنے اسٹیک پر چلتا ہے جس میں رجسٹر محفوظ طریقے سے پکڑے گئے ہیں۔ سی فنکشن کو کال کریں۔ do_syscall_64میں آپ کو دو چیزیں دیتا ہوں۔ pt_regsاور سسٹم کال نمبر جو آپ نے چھوڑا ہے۔ rax.
ڈسپیچ مکمل طور پر وضاحت کرنے کے لئے کافی مختصر ہے. دانا چیک کرتا ہے کہ آیا نمبر رینج میں ہے، اسے ٹھیک کرتا ہے، اور مماثل ہینڈلر کے پاس جاتا ہے۔
if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
regs->ax = x64_sys_call(regs, nr);
}
وہاں دو وضاحتیں درکار ہیں۔
array_index_nospec سپیکٹر تخفیف. قیاس آرائیوں کے تحت CPUs پر، صرف سادہ حدود کی جانچ کافی نہیں ہے۔ اس کی وجہ یہ ہے کہ پروسیسر پہلے چل سکتا ہے اور چیک کے حل ہونے سے پہلے ٹیبل کے آخر میں میموری کو چھو سکتا ہے۔ یہ مددگار انڈیکس فکسشن کو اس طرح سے مجبور کرتا ہے کہ اندازہ لگانا چھوڑا نہیں جا سکتا۔
x64_sys_call بہت سی پرانی وضاحتیں اب بھی غلط ہیں، بشمول کچھ تلاش کے نتائج کے اوپری حصے میں۔ وہ آپ کو بتائیں گے کہ کرنل فنکشن پوائنٹرز کی صف کو انڈیکس کرتا ہے۔ sys_call_table. یہ برسوں سے سچ ہے اور اب ڈسپیچ کا کام ایسا نہیں ہے۔ کرنل 6.9 سے شروع ہو رہا ہے۔ x64_sys_call یہ تخلیق کیا گیا تھا switch براہ راست کالوں کے بارے میں بیان۔
وجہ نتائج کا ایک سلسلہ ہے۔ سپیکٹر تخفیف نے فنکشن پوائنٹرز کے ذریعے بالواسطہ کالیں مہنگی کر دیں۔ اس کی وجہ یہ ہے کہ ہر کال کو retpoline سے گزرنا چاہیے۔
کوئی راستہ نہیں switch براہ راست کال کرنا اس لاگت سے مکمل طور پر بچ جاتا ہے۔ ٹیبل اب بھی موجود ہے کیونکہ ٹریکنگ ٹول اسے استعمال کرتا ہے، لیکن گرم راستہ اب ٹیبل کو نہیں پڑھتا ہے۔ پرانا ٹیبل پر مبنی ڈسپیچ اب بھی میرے 5.15 دانا میں موجود ہے۔ اس لیے اس طرح کے مضامین میں کرنل ورژن کا نام دینا ضروری ہے۔
ہینڈلرز کہاں سے آتے ہیں؟
پروسیسر write نام اٹک گیا۔ __x64_sys_writeیہ نام کرنل سورس میں کہیں بھی درج نہیں ہے۔ میکرو کے ذریعہ تیار کردہ۔
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
SYSCALL_DEFINE3 اس کا مطلب ہے "ایک سسٹم کال جو تین دلائل لیتی ہے”۔ میکرو کو دو افعال کے ساتھ بڑھایا جاتا ہے: ایک حقیقی نفاذ اور ایک پتلا ریپر۔ __x64_sys_write بس آپ کی ضرورت ہے۔ struct pt_regs * اور پھر اس سے دلیل اخذ کریں۔
وہ بالواسطہ کارروائی جان بوجھ کر کی جاتی ہے۔ دلیلوں کے رجسٹروں کو چھوڑنے کے لیے صارف کی جگہ پر بھروسہ کرنے کے بجائے، دانا بالکل ان اقدار کو کھولتا ہے جس کی وہ خود ساختہ ساختوں سے توقع کرتا ہے۔ یہ دفاعی جبلت کے طور پر ایک ہی ہے. array_index_nospecفنکشن کالز کی ظاہری شکل پر لاگو ہوتا ہے۔
آپ کو ایمان پر اس میں سے کچھ لینے کی ضرورت نہیں ہے۔ ftrace، دانا میں بنایا ہوا ایک ٹریسر، دکھاتا ہے کہ کون سے ہینڈلر چل رہے ہیں۔
اس کے لیے جڑ کے خول کی ضرورت ہے۔ sudo وہ فلٹر جو آؤٹ پٹ کو پڑھنے کے قابل رکھتا ہے اس سے مراد شیل کی اپنی پروسیس ID ہے، لہذا ہر لائن پر:
sudo -i
cd /sys/kernel/tracing
echo 0 > tracing_on
echo $$ > set_ftrace_pid # trace only this shell
echo function_graph > current_tracer
echo __x64_sys_write > set_graph_function
echo 1 > tracing_on
echo "trigger a write" > /dev/null # the call we want to catch
echo 0 > tracing_on
head -40 trace
اس کے بغیر set_ftrace_pid ایک چلتا ہوا ڈیسک ٹاپ آپ کے کمپیوٹر پر ہر تحریر کو ٹریک کرے گا جس کے نتیجے میں پڑھنے کے لیے بہت زیادہ آؤٹ پٹ ہوتا ہے۔
ذیل میں ہلکے سے پالش ٹیسٹ سسٹم کے نتائج ہیں۔
9) | __x64_sys_write() {
9) | ksys_write() {
9) | __fdget_pos() {
9) 0.124 us | __fget_light();
9) 0.363 us | }
9) | vfs_write() {
9) | rw_verify_area() {
9) | security_file_permission() {
9) | apparmor_file_permission() {
9) 0.264 us | aa_file_perm();
9) 0.457 us | }
9) 0.644 us | }
9) 0.857 us | }
9) 0.083 us | write_null();
9) | __fsnotify_parent() {
9) 0.107 us | fsnotify();
9) 1.383 us | }
9) 2.813 us | }
9) 3.449 us | }
9) 3.720 us | }
اگر آپ باہر سے پڑھیں تو آپ کو 20 لائنوں میں پوری کہانی مل سکتی ہے۔
__x64_sys_write یہ تیار شدہ ریپر ہے۔ یہ کال کرتا ہے ksys_writeحقیقی نفاذ۔ یہ فائل ڈسکرپٹر کو دیکھے گا۔ __fdget_posپھر vfs_writeورچوئل فائل سسٹم کی پرت وہ ہے جہاں دانا کو اس بات کی پرواہ نہیں ہوتی ہے کہ آپ کس قسم کی چیزیں لکھ رہے ہیں۔
پھر security_file_permission اس مشین کو AppArmor کہتے ہیں کیونکہ یہ Ubuntu چلاتی ہے۔ SELinux سسٹم میں کچھ اور ہے۔ کسی بھی طرح سے، یہ سیکورٹی ماڈیول ہے جو فیصلہ کرتا ہے کہ آیا اس آپریشن کی اجازت دی جائے یا نہیں۔ جب بھی میں لکھتا ہوں۔ ہر کوئی
write_null نتیجہ ہے اور یہ وہاں ہوتا ہے۔ مندرجہ بالا کمانڈ کے برابر ہے: /dev/nullیہ ایک حقیقی ڈرائیور ہے جس کا پورا آپریشن بائٹس پھینک رہا ہے۔ اگر آپ وہی تحریر ڈسک پر موجود کسی فائل کو تفویض کرتے ہیں، تو فائل سسٹم کا فنکشن اس سلاٹ میں ظاہر ہوگا۔ اس پر کچھ نہیں چلتا۔
پوری چیز میں 3.7 مائیکرو سیکنڈ لگے، اور آپ دائیں طرف کے وقت کو دیکھ کر دیکھ سکتے ہیں کہ یہ کہاں گیا تھا۔
جب آپ کام کر لیں، ٹریکر کو واپس اندر رکھیں۔
echo nop > current_tracer
echo > set_graph_function
echo > set_ftrace_pid
اگر /sys/kernel/tracing اس کا نظام میں کوئی وجود نہیں ہے۔ اسے آزمائیں /sys/kernel/debug/tracing اس کے بجائے.
واپسی کا سفر، اور سچائی errno
ہینڈلر نمبر مکمل کرتا ہے اور واپس کرتا ہے۔ وہ نمبر آتا ہے۔ raxاور rax پروگرام صرف وہی چیز ہے جسے آپ واپس لے جاتے ہیں.
اس سے ایسے سوالات اٹھتے ہیں جو شاذ و نادر ہی براہ راست پوچھے جاتے ہیں۔ اگر دانا صرف ایک قدر واپس کر سکتا ہے تو میں کیسے رپورٹ کروں؟ کیا ہوا؟ مزید کہ کیا کچھ گڑبڑ ہے؟
صحیح جواب یہ ہے کہ کوئی الگ چینلز نہیں ہیں۔ دانا نتیجہ کے طور پر اسی رجسٹر میں ایک چھوٹے منفی نمبر کے طور پر غلطی کو لوٹاتا ہے۔ کامیاب write 24 میں سے 24 بائٹس لوٹاتا ہے۔ اے write بند ڈسکرپٹرز کے لیے واپسی -9۔ کیونکہ EBADF غلطی نمبر 9۔
اب حقائق کو یکجا کریں۔ errno یہ موجود ہے، لیکن کچھ شامل نہیں ہوتا ہے۔ errno عمل میں متغیرات۔ دانا متغیرات کو نہیں لکھتا ہے۔
ٹیسٹ مندرجہ ذیل ہے: سیٹ errno اسے 0 پر سیٹ کرنے کے لیے، ایک خام سسٹم کال کریں جس کے ناکام ہونے کی ضمانت ہو اور دونوں قدروں کو دیکھیں۔
#include /* fprintf, stderr */
#include /* errno */
/* raw_write() is the function from the previous section */
errno = 0;
long ok = raw_write(1, "written via raw syscalln", 24);
long bad = raw_write(999, "x", 1); /* not an open descriptor */
fprintf(stderr, "ok = %ldn", ok);
fprintf(stderr, "bad = %ldn", bad);
fprintf(stderr, "errno = %dn", errno);
چلائیں:
written via raw syscall
ok = 24
bad = -9
errno = 0
وہاں یہ ہے۔ کرنل واپس آگیا -9اور errno یہ کبھی منتقل نہیں ہوا۔
errno libc ایک ایجاد ہے۔ جب آپ باقاعدہ فون کال کرتے ہیں۔ write()ریپر چیک کرتا ہے کہ آیا واپسی کی قیمت ایک چھوٹی منفی نمبر ہے۔ اگر ایسا ہے تو، اسے باطل کریں اور نتیجہ کو محفوظ کریں: errnoاور واپس -1 آپ کو. کہ -1اور-ٹھیک ہے-errno وہ پیٹرن جو ہر C پروگرامر سیکھتا ہے وہ اصول ہیں جو مکمل طور پر صارف کی جگہ میں کرنل انٹرفیس کے اوپر بنائے گئے ہیں جو بالکل مختلف طریقے سے کام کرتے ہیں۔
اسے دیکھنے کے بعد، واقف بگ کلاس زیادہ معنی رکھتا ہے۔ errno کال کے ناکام ہونے کے فوراً بعد ہی یہ سمجھ میں آتا ہے۔ کیونکہ ناکام ہونے والا آخری ریپر ایک متغیر تھا جو غلطی سے لکھا گیا تھا۔
دو طرفہ باہر نکلنا
صارف کی جگہ پر واپس جانے کے دو طریقے ہیں: تیز راستہ اور سست راستہ۔
تیز طریقہ ہے۔ sysretآئینہ syscall: انسٹرکشن پوائنٹر کو اس سے بحال کریں: rcx اور اپنا جھنڈا۔ r11 چند چکروں کے بعد یہ واپس 3 پر آ جاتا ہے۔
سست سڑک iretیہ ایک عمومی مقصد میں مداخلت کی واپسی کی کمانڈ ہے۔ یہ کافی سست ہے اور دانا اسے درج ذیل صورتوں میں استعمال کرتا ہے۔ sysret میں اس پر یقین نہیں کر سکتا۔ وجہ آئٹم کوڈ کی خود وضاحت میں بیان کی گئی ہے۔ sysret AMD اور Intel CPUs میں ایک بگ غیر معیاری پتوں کے ساتھ مسائل کا باعث بنتا ہے، جب بھی ذخیرہ شدہ حالت بدل سکتی ہے دانا کو محفوظ راستہ اختیار کرنے پر مجبور کرتا ہے۔ ڈیبگر کے ذریعے پہنچا ptrace رجسٹر تبدیل کرنا ایک عام وجہ ہے۔
کسی بھی کمانڈ پر عمل درآمد سے پہلے، دانا گھر کی دیکھ بھال کے موخر کام انجام دیتا ہے۔ زیر التواء سگنل چیک کریں اور آگے بھیجیں۔ شیڈیولر یہ دیکھنے کے لیے چیک کرتا ہے کہ آیا وہ CPU کو واپس چاہتا ہے، اور اگر ایسا ہے تو، عمل وہیں رک جاتا ہے اور دوسرے کام چلتے ہیں۔
اس کا مطلب یہ ہے کہ سسٹم کالز صرف سروس کی درخواستیں نہیں ہیں۔ یہ ان اہم جگہوں میں سے ایک ہے جہاں کوئی عمل آسانی سے چلنا بند کر سکتا ہے۔ 5 بائٹس لکھنے کی درخواست کی۔ واپسی پر، دانا آپ کے بارے میں ہر چیز پر نظر ثانی کرے گا، بشمول اسے جاری رکھنا چاہیے یا نہیں۔
سسٹم کال جو کبھی نہیں ہوتی
اب واپس کھولنے کے اسرار کی طرف۔
اپنے عمل کے میموری میپ پر ایک نظر ڈالیں۔
cat /proc/self/maps | tail -4
اس کے ساتھ ختم ہوتا ہے:
7fff32f97000-7fff32f9b000 r--p [vvar]
7fff32f9b000-7fff32f9d000 r-xp [vdso]
دو شعبے جو آپ نے نہیں مانگے۔ ان میں سے کوئی بھی آپ کے پروگرام یا لائبریری سے نہیں ہے۔ دانا اسے سسٹم پر ہر عمل میں رکھتا ہے۔
[vdso] ایک ورچوئل ڈائنامک مشترکہ آبجیکٹ کی نمائندگی کرتا ہے۔ یہ ایک چھوٹی، مکمل طور پر مشترکہ لائبریری ہے (علامت ٹیبل کے ساتھ ایک حقیقی ELF) جہاں دانا کسی بھی پتے کی جگہ پر نقشہ بناتا ہے۔ اور دانا ہر عمل کو بتاتا ہے کہ یہ کہاں ہے، لہذا یہ اپنی کاپی کو ڈمپ کر سکتا ہے اور اسے الگ کر سکتا ہے۔
#include
#include /* getauxval, AT_SYSINFO_EHDR */
#include /* getpagesize */
int main(void)
{
void *vdso = (void *)getauxval(AT_SYSINFO_EHDR); /* the kernel tells us where */
size_t len = 2 * getpagesize(); /* the mapping is two pages */
FILE *f = fopen("vdso.so", "wb");
fwrite(vdso, 1, len, f);
fclose(f);
printf("vDSO was mapped at %pn", vdso);
return 0;
}
AT_SYSINFO_EHDR میں رہتے ہیں . اگر آپ اس ہیڈر کو اکیلا چھوڑ دیتے ہیں، تو آپ کو اس کے بارے میں شائستہ انتباہ نہیں ملے گا۔ تعمیر اس طرح رک جاتی ہے: AT_SYSINFO_EHDR undeclared.
اسے چلائیں اور پھر کسی بھی دوسری لائبریری کی طرح سمبل ٹیبل کو پڑھیں۔
./dump_vdso && objdump -T vdso.so | grep __vdso
اور یہ پتہ چلتا ہے:
__vdso_gettimeofday
__vdso_clock_gettime
__vdso_clock_getres
__vdso_time
__vdso_getcpu
ایک جواب ہے۔ clock_gettime یہ اس فہرست میں ہے۔

جب آپ کال کرتے ہیں۔ clock_gettime()libc vDSO کو کال کرتا ہے۔ اگرچہ یہ کوڈ کرنل ڈویلپرز نے لکھا ہے اور کرنل کے ساتھ آتا ہے، یہ عمل کے حصے کے طور پر رنگ 3 میں چلتا ہے۔. موجودہ وقت پڑھتا ہے۔ [vvar] ایک صفحہ لوٹاتا ہے (صرف پڑھنے والا صفحہ جسے کرنل اپ ڈیٹ کرتا رہتا ہے)۔
اجازتوں میں کوئی تبدیلی نہیں ہے۔ syscall ہدایات، انٹری پوائنٹ، اسٹیک سوئچ، یا pt_regs. اور اس کا مطلب ہے کہ کچھ نہیں کیا جا سکتا strace دیکھنے کے لئے، کیونکہ strace یہ باؤنڈری کا مشاہدہ کرکے کام کرتا ہے اور یہ کال کبھی باؤنڈری کے قریب نہیں جاتی۔
بس۔ کرنل میں کچھ آپریشنز ہوتے ہیں جنہیں مسلسل کال کی جاتی ہے اور انہیں کسی اجازت کی ضرورت نہیں ہوتی ہے۔ پڑھیںکرنل نے صرف وہی معلومات واپس کیں جو وہ شائع کرنے کے لیے تیار تھی اور اسے شائع کیا۔
آخری رکاوٹ بتاتی ہے کہ فہرست اتنی مختصر کیوں ہے۔ write() یہ کبھی بھی اس طرح کام نہیں کر سکتا کیونکہ اس کے لیے ریاست کو تبدیل کرنے کی ضرورت ہوتی ہے جو دانا سے تعلق رکھتی ہے۔ گھڑی پڑھنا ایسا نہیں ہے۔ لہذا گھڑی، وقت، اور موجودہ CPU نمبر کو وہاں منتقل کر دیا گیا جہاں کال کرنے والا پہلے سے موجود تھا۔
سرحدی لاگت
مذکورہ بالا تمام میکانزم ہیں۔ پیمائش شدہ قیمتیں درج ذیل ہیں:
بینچ مارک ان کالوں کا موازنہ کرتا ہے جو یقینی طور پر پھنستی ہیں ان کالوں سے جو یقینی طور پر پھنس نہیں پاتی ہیں۔ سب سے پہلے syscall(SYS_getpid). پتلی چیزوں کو توڑنا syscall() ریپر حقیقی چوراہا کو یقینی بناتا ہے۔
/* Excerpt. Needs , and , plus a now()
helper returning seconds as a double, and ITERATIONS defined above. */
double a = now();
for (long i = 0; i < ITERATIONS; i++)
sink += syscall(SYS_getpid);
double b = now();
for (long i = 0; i < ITERATIONS; i++)
clock_gettime(CLOCK_MONOTONIC, &ts);
ٹیسٹ مشین پر ہر ایک کو 2 ملین بار دہرایا جاتا ہے۔
real system call (getpid): 106.9 ns/call
vDSO call (clock_gettime): 17.2 ns/call
ratio: 6.2x
تقریباً 6 بار، getpid یہ سسٹم کال کی طرح سستا ہے۔ پڑھتا ہے اور ایک فیلڈ واپس کرتا ہے۔ اس کا مطلب ہے کہ ان 107 نینو سیکنڈز میں سے تقریباً کوئی بھی کام نہیں کرتا۔ یہ اجازت کی تبدیلی ہے۔ swapgsاسٹیک سوئچ، pt_regs اوپر جانا، واپس نیچے آنا، اور جو بھی تخفیف آپ کے مخصوص CPU کو راستے میں درکار ہے۔
اب، یہاں محتاط رہنے کی چیز ہے، کیونکہ اس میں صرف اعداد کے علاوہ بھی بہت کچھ ہے۔
107 نینو سیکنڈ بہترین کیس کے قریب ہے۔ دیکھیں کہ اس کمپیوٹر نے پہلے کیا رپورٹ کیا ہے۔ Not affected میلٹ ڈاؤن کے لیے، صفحہ ٹیبل کی کوئی تبدیلی نہیں کی جاتی ہے۔ سپیکٹر تخفیف میں شامل ہیں: Enhanced IBRSیہ سافٹ ویئر میں retpoline کے بجائے سلکان میں سنبھالا جاتا ہے۔ یہ CPU دو سب سے مہنگے آپریشنز کو چھوڑ دیتا ہے جو چوراہے میں شامل ہو سکتے ہیں۔
لہذا بینچ مارک خود چلائیں اور اس کے ساتھ اپنی تخفیف کی فائلیں پڑھیں۔
grep . /sys/devices/system/cpu/vulnerabilities/*
اگر آپ کہتے ہیں Mitigation: PTIآپ کی کراسنگ یہاں ماپنے سے کہیں زیادہ کام کر رہی ہے اور آپ کی تعداد زیادہ ہونی چاہیے۔ پرانا سلیکون ڈرامائی طور پر خراب ہوسکتا ہے۔
تناسب کو پائیدار نتائج اور مطلق نمبروں کو مشین سے پڑھنے کے طور پر سمجھیں۔ CPU، دانا، اور تخفیف کی جا رہی کارکردگی کے لحاظ سے نمبرز بدل جاتے ہیں۔ جب آپ وہاں ہوں تو اسے کچھ رنز دیں۔ اس لیپ ٹاپ کا رن گیپ تقریباً 15% تھا، جس سے آپ کو اندازہ ہوتا ہے کہ آپ کو میرے سمیت کسی ایک نمبر پر کتنا بھروسہ کرنا چاہیے۔
ان دعوؤں کو درست کرتا ہے جہاں ایک ہی رن کے ایک سے زیادہ نتائج بڑے پیمانے پر دہرائے جاتے ہیں۔ getpid() سادہ libc پر لاگت مقامی کے برابر ہے۔ syscall(SYS_getpid). glibc دوروں سے بچنے کے لیے ids پراسیس کرتا تھا اور اسے کئی سال پہلے بند کر دیا گیا تھا۔ fork نام کی جگہ کو تبدیل کرنا 100 نینو سیکنڈ ادا کرنے سے بھی بدتر تھا۔
چھ بار یہ خلاصہ لگتا ہے جب تک کہ آپ اسے کسی چیز سے جوڑ دیں۔ ایک ملین ڈالر چھوٹا بنانے کا پروگرام read() ایک کال ایک سیکنڈ کا دسواں حصہ صرف گزرنے میں صرف کرتی ہے۔ یہ بہت سے جدید کرنل انٹرفیس ڈیزائن کے پیچھے دباؤ ہے۔ io_uring اگر آپ ایک ہزار ملازمتیں جمع کراتے ہیں، تو آپ کو ایک ہزار کے بجائے ایک کراس اوور لاگت کا سامنا کرنا پڑ سکتا ہے۔ سسٹم کال بیچنگ، لکھنا بفرنگ، اور استعمال sendfile() پڑھنے لکھنے کے لوپ کے بجائے تمام ایک جیسی اصلاح کی جاتی ہے۔ دوسرے لفظوں میں، یہ کم کام کرنے کے بارے میں نہیں ہے، یہ حد سے کم بار پار کرنے کے بارے میں ہے۔
نتیجہ
اب آپ مجموعی طور پر سسٹم کال کی پیروی کر سکتے ہیں۔ کیا آپ نے کبھی دیکھا ہے؟ syscall میں نے کمانڈ کو اپنی بائنری میں داخل کیا اور دیکھا کہ غلط فائل ڈسکریپٹر اس طرح ظاہر ہوا: -9 جبکہ errno 0 پر رہ کر، ہم نے وی ڈی ایس او کو اس کے اپنے ایڈریس اسپیس سے باہر لے لیا، اس کے سمبل ٹیبل کو پڑھا، اور پیمائش کی کہ اس کے اپنے سی پی یو پر چوراہے کی قیمت کیا ہوگی۔
زیادہ مفید طور پر، ایک ذہنی ماڈل ہے جو بار بار ادا کرتا ہے۔ جب آپ اسے پڑھتے ہیں۔ io_uring سسٹم کال اوور ہیڈ کو کم کرنے سے آپ کو بالکل پتہ چلتا ہے کہ اوور ہیڈ کا کیا مطلب ہے۔ اگر آپ کا پروفائل وقت دکھاتا ہے۔ entry_SYSCALL_64آپ جانتے ہیں کہ اس کا کام کیا ہے۔ جب strace اگر آپ کو کچھ نظر نہیں آتا ہے تو، ٹول پر شک کرنے سے پہلے vDSO کو چیک کرنا اچھا خیال ہوگا۔
آپ یہاں سے چند سمت جا سکتے ہیں۔ پھانسی ftrace ہدایت اور مندرجہ ذیل __x64_sys_write فائل سسٹم کے درجہ بندی کے نیچے جائیں۔ پڑھیں arch/x86/entry/entry_64.S: اچھی طرح سے تبصرہ کیا گیا اور اس کی ساکھ سے کہیں زیادہ قابل رسائی۔ یا اسے چیک کریں۔ /sys/devices/system/cpu/vulnerabilities/ پرانے نظام میں اس باؤنڈری پر ہر تخفیف کی لاگت کا حساب لگائیں۔
ایپیلاگ
میں فی الحال لینکس کرنل کے اوپر ایک OS ڈیزائن کرنے کا تجربہ کر رہا ہوں جو Debian ایکو سسٹم کے ساتھ 100% ہم آہنگ ہونے کی کوشش کرتے ہوئے اینڈرائیڈ طرز کی اجازتوں اور صلاحیتوں کے ماڈل کو ڈیسک ٹاپ OS پر لاتا ہے۔ اس کی وجہ سے حال ہی میں کچھ بہت ہی دلچسپ تحقیق ہوئی۔ یہ مضمون اسی تحقیق کا نتیجہ ہے۔
میں رسمی تصدیق پر جانے سے پہلے لینکس کرنل کے بارے میں مزید لکھوں گا، جیسا کہ DO-178C اور DO-333 دنیا میں ہے جہاں ایویونکس سافٹ ویئر کو ٹولز کے ساتھ اس کی تصدیق کے لیے اہل ہونا ضروری ہے۔ عام طور پر اس کا مطلب ہے وائپر، Why3، Z3 اور دوست۔
اسی وقت، میں thechris.in پر ان سسٹمز کے بارے میں لکھ رہا ہوں جن کا حقیقت سے رابطہ برقرار رہنا چاہیے۔ اس میں یہ بھی شامل ہے کہ تجریدوں پر تعمیر کرنے کا کیا مطلب ہے جن کے اخراجات کو ناپا جا سکتا ہے لیکن دیکھا نہیں جا سکتا۔