Amin'sTechLab
💻 یکی از اشتباهات رایج در زبان C: برگرداندن آدرس یک متغیر Local از تابع (بخش اول) یکی از باگهای خطرناک در زبان C، استفاده از اشارهگری است که به یک متغیر محلی (Local Variable) اشاره میکند. مثلاً: #include <stdio.h> int *getNumber() { int x = 42; …
💻 چرا Dangling Pointerها در C خیلی خطرناک هستند؟ (بخش دوم)
یکی از دلایلی که این نوع خطاها سخت پیدا میشوند این است که برنامه همیشه Crash نمیکند.
مثلاً:
این کد اشتباه است.
چون آرایه:
داخل Stack ساخته میشود.
بعد از خروج از تابع:
این حافظه دیگر معتبر نیست.
اما اشارهگر هنوز همان آدرس قبلی را نگه داشته است.
حالا ممکن است:
گاهی درست کار کند.
گاهی خروجی خراب بدهد:
و گاهی برنامه Crash کند.
حتی تغییرهای ساده میتوانند رفتار برنامه را تغییر دهند:
اضافه کردن همین تابع ممکن است باعث شود حافظه قبلی Stack دوباره استفاده شود و مقدار اشارهگر شما تغییر کند.
مشکل از کامپایلر نیست؛
برنامه شما دارد از حافظهای استفاده میکند که دیگر وجود ندارد.
در بخش آخر روشهای صحیح مدیریت این شرایط را بررسی میکنیم.
@Amin_TechLab
یکی از دلایلی که این نوع خطاها سخت پیدا میشوند این است که برنامه همیشه Crash نمیکند.
مثلاً:
char *getName()
{
char name[] = "Embedded";
return name;
}
این کد اشتباه است.
چون آرایه:
char name[]
داخل Stack ساخته میشود.
بعد از خروج از تابع:
getName()
|
v
+---------------+
| name[] |
+---------------+
این حافظه دیگر معتبر نیست.
اما اشارهگر هنوز همان آدرس قبلی را نگه داشته است.
حالا ممکن است:
printf("%s", getName());گاهی درست کار کند.
گاهی خروجی خراب بدهد:
Embe@#12
و گاهی برنامه Crash کند.
حتی تغییرهای ساده میتوانند رفتار برنامه را تغییر دهند:
void test()
{
int buffer[100];
}
اضافه کردن همین تابع ممکن است باعث شود حافظه قبلی Stack دوباره استفاده شود و مقدار اشارهگر شما تغییر کند.
مشکل از کامپایلر نیست؛
برنامه شما دارد از حافظهای استفاده میکند که دیگر وجود ندارد.
در بخش آخر روشهای صحیح مدیریت این شرایط را بررسی میکنیم.
@Amin_TechLab
❤2🔥2👾2
💻 روش صحیح مدیریت طول عمر حافظه در زبان C (بخش سوم)
اگر میخواهید مقداری بعد از خروج از تابع باقی بماند، نباید آدرس یک متغیر Local را برگردانید.
چند روش صحیح وجود دارد:
✅ روش اول: استفاده از
متغیرهای
✅ روش دوم: استفاده از حافظه Dynamic
اما مسئولیت آزاد کردن حافظه با شماست:
اگر این کار انجام نشود، مشکل دیگری به نام:
Memory Leak
ایجاد میشود.
✅ روش سوم: ارسال Buffer توسط Caller
این روش در برنامههای Embedded بسیار رایج است:
در این حالت کسی که حافظه را ساخته، طول عمر آن را هم کنترل میکند.
در زبان C همیشه یک سؤال مهم وجود دارد:
این حافظه متعلق به چه کسی است و تا چه زمانی معتبر است؟
بسیاری از خطاهای سخت در Embedded و سیستمهای Low-Level به دلیل پاسخ اشتباه به همین سؤال ایجاد میشوند.
یک Pointer فقط یک آدرس نیست؛
یک قرارداد درباره مالکیت و طول عمر حافظه است. ⚠️
@Amin_TechLab
اگر میخواهید مقداری بعد از خروج از تابع باقی بماند، نباید آدرس یک متغیر Local را برگردانید.
چند روش صحیح وجود دارد:
✅ روش اول: استفاده از
staticchar *getName()
{
static char name[] = "Embedded";
return name;
}
متغیرهای
static تا پایان اجرای برنامه در حافظه باقی میمانند.✅ روش دوم: استفاده از حافظه Dynamic
#include <stdlib.h>
#include <string.h>
char *getName()
{
char *name = malloc(20);
strcpy(name, "Embedded");
return name;
}
اما مسئولیت آزاد کردن حافظه با شماست:
free(name);
اگر این کار انجام نشود، مشکل دیگری به نام:
Memory Leak
ایجاد میشود.
✅ روش سوم: ارسال Buffer توسط Caller
این روش در برنامههای Embedded بسیار رایج است:
void getName(char *buffer)
{
strcpy(buffer, "Embedded");
}
در این حالت کسی که حافظه را ساخته، طول عمر آن را هم کنترل میکند.
در زبان C همیشه یک سؤال مهم وجود دارد:
این حافظه متعلق به چه کسی است و تا چه زمانی معتبر است؟
بسیاری از خطاهای سخت در Embedded و سیستمهای Low-Level به دلیل پاسخ اشتباه به همین سؤال ایجاد میشوند.
یک Pointer فقط یک آدرس نیست؛
یک قرارداد درباره مالکیت و طول عمر حافظه است. ⚠️
@Amin_TechLab
🔥3👾3❤2👍1
پشت صحنهی یک دستور در لینوکس! (۱/۱۰)
وقتی در لینوکس ترمینال را باز میکنید و یک دستور مثل
قبل از اینکه سراغ شل برویم، باید کمی به گذشته برگردیم.
دهههای ۱۹۵۰ و ۱۹۶۰ چیزی به اسم «ترمینال» اصلاً یک پنجرهی نرمافزاری نبود؛ بلکه یک دستگاه فیزیکی بود که تنها وظیفهاش دریافت ورودی از کاربر و نمایش خروجی کامپیوتر بود.
در آن زمان کاربر متن را با صفحهکلید وارد میکرد، دادهها از طریق سیگنالهای الکتریکی به کامپیوتر مرکزی ارسال میشدند و پس از پردازش، پاسخ دوباره به دستگاه برمیگشت تا روی رولهای کاغذ چاپ شود.
این دستگاهها که با نام Teletype (TTY) شناخته میشدند، سرعت بالایی هم نداشتند؛ معمولاً چیزی حدود ۱۰ تا ۲۰ کاراکتر در ثانیه چاپ میکردند.
چند سال بعد ترمینالهای ویدیویی معرفی شدند. دیگر خبری از رولهای کاغذی نبود و خروجی مستقیماً روی نمایشگرهای CRT نمایش داده میشد. دستگاههایی مانند DEC VT100 یکی از معروفترین نمونههای این نسل بودند و تجربهی کار با کامپیوتر را متحول کردند.
ادامه دارد....
@Amin_Techlab
وقتی در لینوکس ترمینال را باز میکنید و یک دستور مثل
ls یا pwd را اجرا میکنید، واقعاً چه اتفاقی میافتد؟قبل از اینکه سراغ شل برویم، باید کمی به گذشته برگردیم.
دهههای ۱۹۵۰ و ۱۹۶۰ چیزی به اسم «ترمینال» اصلاً یک پنجرهی نرمافزاری نبود؛ بلکه یک دستگاه فیزیکی بود که تنها وظیفهاش دریافت ورودی از کاربر و نمایش خروجی کامپیوتر بود.
در آن زمان کاربر متن را با صفحهکلید وارد میکرد، دادهها از طریق سیگنالهای الکتریکی به کامپیوتر مرکزی ارسال میشدند و پس از پردازش، پاسخ دوباره به دستگاه برمیگشت تا روی رولهای کاغذ چاپ شود.
این دستگاهها که با نام Teletype (TTY) شناخته میشدند، سرعت بالایی هم نداشتند؛ معمولاً چیزی حدود ۱۰ تا ۲۰ کاراکتر در ثانیه چاپ میکردند.
چند سال بعد ترمینالهای ویدیویی معرفی شدند. دیگر خبری از رولهای کاغذی نبود و خروجی مستقیماً روی نمایشگرهای CRT نمایش داده میشد. دستگاههایی مانند DEC VT100 یکی از معروفترین نمونههای این نسل بودند و تجربهی کار با کامپیوتر را متحول کردند.
ادامه دارد....
@Amin_Techlab
❤2🔥2👾2
Amin'sTechLab
پشت صحنهی یک دستور در لینوکس! (۱/۱۰) وقتی در لینوکس ترمینال را باز میکنید و یک دستور مثل ls یا pwd را اجرا میکنید، واقعاً چه اتفاقی میافتد؟ قبل از اینکه سراغ شل برویم، باید کمی به گذشته برگردیم. دهههای ۱۹۵۰ و ۱۹۶۰ چیزی به اسم «ترمینال» اصلاً یک پنجرهی…
پشت صحنهی یک دستور در لینوکس!! (۲/۱۰)
امروزه دیگر آن دستگاههای بزرگ وجود ندارند، اما مفهوم آنها همچنان زنده است.
وقتی برنامهای مثل GNOME Terminal، Konsole یا Windows Terminal را باز میکنید، در واقع با یک Terminal Emulator کار میکنید؛ نرمافزاری که رفتار همان ترمینالهای قدیمی را شبیهسازی میکند.
به همین دلیل هنوز هم اصطلاحاتی مانند TTY و PTS را در لینوکس میبینیم؛ یادگار روزهایی که ترمینال یک سختافزار واقعی بود.
اما یک نکتهی مهم وجود دارد:
ترمینال هیچ دستوری را اجرا نمیکند.
وظیفهی آن فقط دریافت ورودی و نمایش خروجی است.
اجرای دستورات بر عهدهی برنامهای دیگر به نام Shell است.
شل نه سیستمعامل است و نه کرنل؛ فقط یک برنامه است، درست مثل هر برنامهی دیگری.
کلمهی Shell به معنی «پوسته» است؛ لایهای که بین کاربر و کرنل قرار میگیرد و ارتباط میان آنها را برقرار میکند.
مثلاً وقتی دستور زیر را اجرا میکنید:
شل ابتدا بررسی میکند که فایل اجرایی
برای این کار از متغیر محیطی PATH استفاده میکند:
خروجی چیزی شبیه این خواهد بود:
شل این مسیرها را بهترتیب جستجو میکند تا فایل اجرایی موردنظر را پیدا کند.
به همین دلیل اگر یک فایل اجرایی داخل پوشهی فعلی داشته باشید، فقط نوشتن نام آن کافی نیست و باید مسیرش را هم مشخص کنید:
همین طراحی باعث میشود فایلهای ناشناس داخل پوشهی فعلی بهصورت تصادفی اجرا نشوند و امنیت سیستم حفظ شود.
ادامه دارد ....
@Amin_TechLab
امروزه دیگر آن دستگاههای بزرگ وجود ندارند، اما مفهوم آنها همچنان زنده است.
وقتی برنامهای مثل GNOME Terminal، Konsole یا Windows Terminal را باز میکنید، در واقع با یک Terminal Emulator کار میکنید؛ نرمافزاری که رفتار همان ترمینالهای قدیمی را شبیهسازی میکند.
به همین دلیل هنوز هم اصطلاحاتی مانند TTY و PTS را در لینوکس میبینیم؛ یادگار روزهایی که ترمینال یک سختافزار واقعی بود.
اما یک نکتهی مهم وجود دارد:
ترمینال هیچ دستوری را اجرا نمیکند.
وظیفهی آن فقط دریافت ورودی و نمایش خروجی است.
اجرای دستورات بر عهدهی برنامهای دیگر به نام Shell است.
شل نه سیستمعامل است و نه کرنل؛ فقط یک برنامه است، درست مثل هر برنامهی دیگری.
کلمهی Shell به معنی «پوسته» است؛ لایهای که بین کاربر و کرنل قرار میگیرد و ارتباط میان آنها را برقرار میکند.
مثلاً وقتی دستور زیر را اجرا میکنید:
ls
شل ابتدا بررسی میکند که فایل اجرایی
ls کجا قرار دارد.برای این کار از متغیر محیطی PATH استفاده میکند:
echo $PATH
خروجی چیزی شبیه این خواهد بود:
/usr/local/bin:/usr/bin:/bin
شل این مسیرها را بهترتیب جستجو میکند تا فایل اجرایی موردنظر را پیدا کند.
به همین دلیل اگر یک فایل اجرایی داخل پوشهی فعلی داشته باشید، فقط نوشتن نام آن کافی نیست و باید مسیرش را هم مشخص کنید:
./my_app
همین طراحی باعث میشود فایلهای ناشناس داخل پوشهی فعلی بهصورت تصادفی اجرا نشوند و امنیت سیستم حفظ شود.
ادامه دارد ....
@Amin_TechLab
❤4👾2
Amin'sTechLab
پشت صحنهی یک دستور در لینوکس!! (۲/۱۰) امروزه دیگر آن دستگاههای بزرگ وجود ندارند، اما مفهوم آنها همچنان زنده است. وقتی برنامهای مثل GNOME Terminal، Konsole یا Windows Terminal را باز میکنید، در واقع با یک Terminal Emulator کار میکنید؛ نرمافزاری که رفتار…
پشت صحنهی یک دستور در لینوکس! (۳/۱۰)
ممکن است این سؤال برایتان پیش بیاید که آیا فقط یک شل وجود دارد؟
از آنجایی که شل خودش فقط یک برنامه است، هر توسعهدهندهای میتواند نسخهی مخصوص خودش را بنویسد. به همین دلیل طی سالها شلهای مختلفی با امکانات متفاوت به وجود آمدهاند.
• sh (Bourne Shell)
قدیمیترین شل استاندارد یونیکس که در سال ۱۹۷۷ معرفی شد. اگرچه امکاناتش محدود است، اما هنوز هم پایهی بسیاری از اسکریپتهای سیستمی محسوب میشود.
• Bash (Bourne Again Shell)
محبوبترین شل دنیای لینوکس و انتخاب پیشفرض بسیاری از توزیعها. قابلیتهایی مثل History، تکمیل خودکار با کلید Tab و امکانات پیشرفتهی اسکریپتنویسی باعث محبوبیت آن شده است.
• Zsh (Z Shell)
شل مدرنی که این روزها در macOS و بسیاری از سیستمهای توسعه استفاده میشود. اگر نام Oh My Zsh را شنیده باشید، با یکی از معروفترین مجموعه افزونههای همین شل آشنا هستید.
• Fish (Friendly Interactive Shell)
اگر به دنبال تجربهای سادهتر و کاربرپسندتر باشید، Fish یکی از بهترین انتخابهاست. پیشنهاد خودکار دستورات، رنگبندی مناسب و تنظیمات پیشفرض خوب، آن را برای کاربران تازهکار و حرفهای جذاب کرده است.
اگر این شلها روی سیستم نصب باشند، کافی است نام آنها را در ترمینال اجرا کنید:
یا
برای تغییر دائمی شل پیشفرض نیز میتوانید از دستور زیر استفاده کنید:
در نهایت، همهی این شلها یک هدف مشترک دارند؛ برقراری ارتباط بین شما و سیستمعامل، فقط با روشها و امکانات متفاوت.
ادامه دارد...
@Amin_TechLab
ممکن است این سؤال برایتان پیش بیاید که آیا فقط یک شل وجود دارد؟
از آنجایی که شل خودش فقط یک برنامه است، هر توسعهدهندهای میتواند نسخهی مخصوص خودش را بنویسد. به همین دلیل طی سالها شلهای مختلفی با امکانات متفاوت به وجود آمدهاند.
• sh (Bourne Shell)
قدیمیترین شل استاندارد یونیکس که در سال ۱۹۷۷ معرفی شد. اگرچه امکاناتش محدود است، اما هنوز هم پایهی بسیاری از اسکریپتهای سیستمی محسوب میشود.
• Bash (Bourne Again Shell)
محبوبترین شل دنیای لینوکس و انتخاب پیشفرض بسیاری از توزیعها. قابلیتهایی مثل History، تکمیل خودکار با کلید Tab و امکانات پیشرفتهی اسکریپتنویسی باعث محبوبیت آن شده است.
• Zsh (Z Shell)
شل مدرنی که این روزها در macOS و بسیاری از سیستمهای توسعه استفاده میشود. اگر نام Oh My Zsh را شنیده باشید، با یکی از معروفترین مجموعه افزونههای همین شل آشنا هستید.
• Fish (Friendly Interactive Shell)
اگر به دنبال تجربهای سادهتر و کاربرپسندتر باشید، Fish یکی از بهترین انتخابهاست. پیشنهاد خودکار دستورات، رنگبندی مناسب و تنظیمات پیشفرض خوب، آن را برای کاربران تازهکار و حرفهای جذاب کرده است.
اگر این شلها روی سیستم نصب باشند، کافی است نام آنها را در ترمینال اجرا کنید:
fish
یا
zsh
برای تغییر دائمی شل پیشفرض نیز میتوانید از دستور زیر استفاده کنید:
chsh
در نهایت، همهی این شلها یک هدف مشترک دارند؛ برقراری ارتباط بین شما و سیستمعامل، فقط با روشها و امکانات متفاوت.
ادامه دارد...
@Amin_TechLab
❤4👾2
Amin'sTechLab
پشت صحنهی یک دستور در لینوکس! (۳/۱۰) ممکن است این سؤال برایتان پیش بیاید که آیا فقط یک شل وجود دارد؟ از آنجایی که شل خودش فقط یک برنامه است، هر توسعهدهندهای میتواند نسخهی مخصوص خودش را بنویسد. به همین دلیل طی سالها شلهای مختلفی با امکانات متفاوت به…
پشت صحنهی یک دستور در لینوکس! (۴/۱۰)
یکی از زیباترین ایدههایی که فلسفهی یونیکس بر پایهی آن شکل گرفته، مفهوم Pipeline است.
اصل ماجرا ساده است:
حالا سؤال اینجاست؛ اگر هر برنامه فقط یک وظیفه داشته باشد، چطور کارهای پیچیده انجام میشوند؟
پاسخ را شل میدهد.
هر برنامه در لینوکس سه کانال استاندارد دارد:
stdin → ورودی برنامه
stdout → خروجی عادی
stderr → خروجی خطا
شل میتواند خروجی یک برنامه را مستقیماً به ورودی برنامهی بعدی متصل کند.
مثلاً:
در اینجا خروجی دستور
بدون اینکه فایل موقتی ساخته شود یا برنامهی بزرگی نوشته شود.
به همین دلیل ابزارهای یونیکس مثل قطعات لگو هستند؛ هر کدام یک کار انجام میدهند و شل آنها را کنار هم قرار میدهد تا یک کار بزرگتر ساخته شود.
همین سادگی، یکی از مهمترین دلایل ماندگاری فلسفهی یونیکس است.
ادامه دارد....
@Amin_TechLab
یکی از زیباترین ایدههایی که فلسفهی یونیکس بر پایهی آن شکل گرفته، مفهوم Pipeline است.
اصل ماجرا ساده است:
هر برنامه فقط یک کار را انجام دهد، اما همان یک کار را به بهترین شکل ممکن انجام دهد.
حالا سؤال اینجاست؛ اگر هر برنامه فقط یک وظیفه داشته باشد، چطور کارهای پیچیده انجام میشوند؟
پاسخ را شل میدهد.
هر برنامه در لینوکس سه کانال استاندارد دارد:
stdin → ورودی برنامه
stdout → خروجی عادی
stderr → خروجی خطا
شل میتواند خروجی یک برنامه را مستقیماً به ورودی برنامهی بعدی متصل کند.
مثلاً:
cat /var/log/mylog | grep "error"
در اینجا خروجی دستور
cat مستقیماً وارد grep میشود و grep فقط خطوطی را نمایش میدهد که شامل کلمهی error هستند.بدون اینکه فایل موقتی ساخته شود یا برنامهی بزرگی نوشته شود.
به همین دلیل ابزارهای یونیکس مثل قطعات لگو هستند؛ هر کدام یک کار انجام میدهند و شل آنها را کنار هم قرار میدهد تا یک کار بزرگتر ساخته شود.
همین سادگی، یکی از مهمترین دلایل ماندگاری فلسفهی یونیکس است.
ادامه دارد....
@Amin_TechLab
❤2👾2🔥1
Amin'sTechLab
پشت صحنهی یک دستور در لینوکس! (۴/۱۰) یکی از زیباترین ایدههایی که فلسفهی یونیکس بر پایهی آن شکل گرفته، مفهوم Pipeline است. اصل ماجرا ساده است: هر برنامه فقط یک کار را انجام دهد، اما همان یک کار را به بهترین شکل ممکن انجام دهد. حالا سؤال اینجاست؛ اگر هر…
پشت صحنهی یک دستور در لینوکس! (۵/۱۰)
وقتی Enter را فشار میدهید، اولین چیزی که به شل میرسد چیست؟
از دید ما فقط یک خط متن تایپ شده است؛ اما پشت پرده چندین مرحله اتفاق میافتد.
ابتدا ترمینال کلیدهایی که فشار دادهایم را دریافت میکند.
این دادهها از طریق TTY به برنامهای ارسال میشوند که در Foreground ترمینال قرار دارد؛ معمولاً همان شل.
در نتیجه شل مستقیماً از صفحهکلید چیزی دریافت نمیکند، بلکه اطلاعات را از Standard Input خودش میخواند.
فرض کنید این دستور را وارد میکنیم:
در این لحظه هنوز هیچ برنامهای اجرا نشده است.
شل فقط یک رشتهی متنی دریافت کرده است.
حالا باید تصمیم بگیرد:
دستور چیست؟
آرگومانها کداماند؟
آیا این دستور داخلی است یا باید فایل اجرایی پیدا شود؟
به همین دلیل شل فقط یک اجراکنندهی برنامه نیست؛ بلکه قبل از هر چیز، یک مفسر (Interpreter) است که باید معنی متن واردشده را درک کند.
ادامه دارد ...
@Amin_TechLab
وقتی Enter را فشار میدهید، اولین چیزی که به شل میرسد چیست؟
از دید ما فقط یک خط متن تایپ شده است؛ اما پشت پرده چندین مرحله اتفاق میافتد.
ابتدا ترمینال کلیدهایی که فشار دادهایم را دریافت میکند.
این دادهها از طریق TTY به برنامهای ارسال میشوند که در Foreground ترمینال قرار دارد؛ معمولاً همان شل.
در نتیجه شل مستقیماً از صفحهکلید چیزی دریافت نمیکند، بلکه اطلاعات را از Standard Input خودش میخواند.
فرض کنید این دستور را وارد میکنیم:
echo myfile
در این لحظه هنوز هیچ برنامهای اجرا نشده است.
شل فقط یک رشتهی متنی دریافت کرده است.
حالا باید تصمیم بگیرد:
دستور چیست؟
آرگومانها کداماند؟
آیا این دستور داخلی است یا باید فایل اجرایی پیدا شود؟
به همین دلیل شل فقط یک اجراکنندهی برنامه نیست؛ بلکه قبل از هر چیز، یک مفسر (Interpreter) است که باید معنی متن واردشده را درک کند.
ادامه دارد ...
@Amin_TechLab
❤4👾1
Amin'sTechLab
پشت صحنهی یک دستور در لینوکس! (۵/۱۰) وقتی Enter را فشار میدهید، اولین چیزی که به شل میرسد چیست؟ از دید ما فقط یک خط متن تایپ شده است؛ اما پشت پرده چندین مرحله اتفاق میافتد. ابتدا ترمینال کلیدهایی که فشار دادهایم را دریافت میکند. این دادهها از طریق TTY…
پشت صحنهی یک دستور در لینوکس! (۶/۱۰)
قبل از اجرای هر دستور، شل یک مرحلهی بسیار مهم را انجام میدهد:
Parsing و Expansion
برای مثال این چهار دستور را ببینید:
ظاهر آنها تقریباً مشابه است، اما شل آنها را کاملاً متفاوت تفسیر میکند.
شل باید تشخیص دهد:
آیا متغیر محیطی باید جایگزین شود؟
آیا نقلقولها مانع Expansion هستند؟
آیا
در واقع برنامهی
تمام این تبدیلها قبل از اجرای برنامه توسط خود شل انجام میشود.
به همین دلیل چیزی که برنامه دریافت میکند، الزاماً همان متنی نیست که شما تایپ کردهاید.
در حقیقت، شل ابتدا متن خام را به یک دستور قابل فهم تبدیل میکند و بعد اجرای آن را آغاز میکند.
ادامه دارد...
@Amin_TechLab
قبل از اجرای هر دستور، شل یک مرحلهی بسیار مهم را انجام میدهد:
Parsing و Expansion
برای مثال این چهار دستور را ببینید:
echo $HOME
echo "$HOME"
echo '$HOME'
echo *.txt
ظاهر آنها تقریباً مشابه است، اما شل آنها را کاملاً متفاوت تفسیر میکند.
شل باید تشخیص دهد:
آیا متغیر محیطی باید جایگزین شود؟
آیا نقلقولها مانع Expansion هستند؟
آیا
* باید به لیست فایلها تبدیل شود؟در واقع برنامهی
echo هیچ شناختی از $HOME یا *.txt ندارد.تمام این تبدیلها قبل از اجرای برنامه توسط خود شل انجام میشود.
به همین دلیل چیزی که برنامه دریافت میکند، الزاماً همان متنی نیست که شما تایپ کردهاید.
در حقیقت، شل ابتدا متن خام را به یک دستور قابل فهم تبدیل میکند و بعد اجرای آن را آغاز میکند.
ادامه دارد...
@Amin_TechLab
❤2👾2
Amin'sTechLab
پشت صحنهی یک دستور در لینوکس! (۶/۱۰) قبل از اجرای هر دستور، شل یک مرحلهی بسیار مهم را انجام میدهد: Parsing و Expansion برای مثال این چهار دستور را ببینید: echo $HOME echo "$HOME" echo '$HOME' echo *.txt ظاهر آنها تقریباً مشابه است، اما شل آنها را کاملاً…
پشت صحنهی یک دستور در لینوکس! (۷/۱۰)
بعد از اینکه شل تصمیم گرفت چه برنامهای باید اجرا شود، نوبت به دو مفهوم بسیار مهم در سیستمعاملهای شبهیونیکس میرسد:
fork و exec
فرض کنید دستور زیر را اجرا میکنید:
اولین کاری که شل انجام میدهد، ایجاد یک فرآیند جدید است.
این کار با سیستمکال fork() انجام میشود.
بعد از آن، فرآیند جدید با استفاده از exec() خودش را کاملاً با برنامهی
در همین زمان، شل اصلی منتظر میماند تا اجرای برنامه تمام شود و سپس دوباره کنترل ترمینال را در اختیار بگیرد.
اما همهی دستورات این مسیر را طی نمیکنند.
برخی دستورات اصلاً فایل اجرایی مستقلی ندارند و داخل خود شل پیادهسازی شدهاند؛ به اینها Shell Builtins گفته میشود.
یکی از بهترین مثالها دستور
اگر
به همین دلیل دستوراتی مانند:
توسط خود شل اجرا میشوند و نیازی به جستجو در PATH ندارند.
ادامه دارد...
@Amin_TechLab
بعد از اینکه شل تصمیم گرفت چه برنامهای باید اجرا شود، نوبت به دو مفهوم بسیار مهم در سیستمعاملهای شبهیونیکس میرسد:
fork و exec
فرض کنید دستور زیر را اجرا میکنید:
ls
اولین کاری که شل انجام میدهد، ایجاد یک فرآیند جدید است.
این کار با سیستمکال fork() انجام میشود.
بعد از آن، فرآیند جدید با استفاده از exec() خودش را کاملاً با برنامهی
ls جایگزین میکند.در همین زمان، شل اصلی منتظر میماند تا اجرای برنامه تمام شود و سپس دوباره کنترل ترمینال را در اختیار بگیرد.
اما همهی دستورات این مسیر را طی نمیکنند.
برخی دستورات اصلاً فایل اجرایی مستقلی ندارند و داخل خود شل پیادهسازی شدهاند؛ به اینها Shell Builtins گفته میشود.
یکی از بهترین مثالها دستور
cd است.اگر
cd یک برنامهی مستقل بود، فقط مسیر کاری همان فرآیند فرزند تغییر میکرد و بعد از پایان برنامه، شل همچنان در همان پوشهی قبلی باقی میماند.به همین دلیل دستوراتی مانند:
cdpwdaliasexportتوسط خود شل اجرا میشوند و نیازی به جستجو در PATH ندارند.
ادامه دارد...
@Amin_TechLab
🔥2👾2❤1
Amin'sTechLab
پشت صحنهی یک دستور در لینوکس! (۷/۱۰) بعد از اینکه شل تصمیم گرفت چه برنامهای باید اجرا شود، نوبت به دو مفهوم بسیار مهم در سیستمعاملهای شبهیونیکس میرسد: fork و exec فرض کنید دستور زیر را اجرا میکنید: ls اولین کاری که شل انجام میدهد، ایجاد یک فرآیند جدید…
پشت صحنهی یک دستور در لینوکس! (۸/۱۰)
شل فقط مسئول اجرای برنامهها نیست؛ خودش یک زبان برنامهنویسی کامل است.
برای مثال:
یا:
و حتی:
در این مثالها دیگر فقط در حال اجرای یک برنامه نیستیم.
شل متغیر تعریف میکند، شرط اجرا میکند، حلقه میسازد و بر اساس قواعد زبانی خودش تصمیم میگیرد چه اتفاقی بیفتد.
به همین دلیل Shell Script فقط مجموعهای از دستورات پشت سر هم نیست؛ بلکه یک زبان اسکریپتنویسی واقعی است.
با استفاده از آن میتوان برنامههای مختلف را به هم متصل کرد، روی خروجی آنها تصمیم گرفت، عملیات تکراری را خودکار کرد و حتی ابزارهای نسبتاً پیچیده ساخت.
همین ویژگی است که شل را از یک Command Runner ساده فراتر میبرد.
ادامه دارد...
@Amin_TechLab
شل فقط مسئول اجرای برنامهها نیست؛ خودش یک زبان برنامهنویسی کامل است.
برای مثال:
name="Ali"
echo "$name"
یا:
if [ -f /etc/passwd ]; then
echo "exists"
fi
و حتی:
for file in *.txt; do
echo "$file"
done
در این مثالها دیگر فقط در حال اجرای یک برنامه نیستیم.
شل متغیر تعریف میکند، شرط اجرا میکند، حلقه میسازد و بر اساس قواعد زبانی خودش تصمیم میگیرد چه اتفاقی بیفتد.
به همین دلیل Shell Script فقط مجموعهای از دستورات پشت سر هم نیست؛ بلکه یک زبان اسکریپتنویسی واقعی است.
با استفاده از آن میتوان برنامههای مختلف را به هم متصل کرد، روی خروجی آنها تصمیم گرفت، عملیات تکراری را خودکار کرد و حتی ابزارهای نسبتاً پیچیده ساخت.
همین ویژگی است که شل را از یک Command Runner ساده فراتر میبرد.
ادامه دارد...
@Amin_TechLab
👾3❤1🔥1
Amin'sTechLab
پشت صحنهی یک دستور در لینوکس! (۸/۱۰) شل فقط مسئول اجرای برنامهها نیست؛ خودش یک زبان برنامهنویسی کامل است. برای مثال: name="Ali" echo "$name" یا: if [ -f /etc/passwd ]; then echo "exists" fi و حتی: for file in *.txt; do echo "$file" done در این مثالها…
پشت صحنهی یک دستور در لینوکس! (۹/۱۰)
کار شل با اجرای یک برنامه تمام نمیشود.
اگر برنامه در Foreground اجرا شده باشد، شل تا پایان اجرای آن منتظر میماند و کنترل ترمینال را دوباره پس میگیرد.
به همین دلیل هنگام اجرای برنامههای زمانبر، تا پایان کار خبری از Prompt جدید نیست.
اما وظایف شل فقط به انتظار کشیدن محدود نمیشود.
اگر کاربر کلیدهای Ctrl+C یا Ctrl+Z را فشار دهد، این شل است که باید سیگنال مناسب را مدیریت کند.
همچنین اگر برنامهای را با
او باید:
وضعیت Jobها را نگه دارد.
پایان اجرای آنها را تشخیص دهد.
در صورت نیاز به کاربر اطلاع دهد.
و همزمان Prompt را برای اجرای دستورات بعدی نمایش دهد.
به همین دلیل شل صرفاً یک اجراکنندهی برنامه نیست؛ بلکه مدیر چرخهی اجرای آنها نیز محسوب میشود.
ادامه دارد...
@Amin_TechLab
کار شل با اجرای یک برنامه تمام نمیشود.
اگر برنامه در Foreground اجرا شده باشد، شل تا پایان اجرای آن منتظر میماند و کنترل ترمینال را دوباره پس میگیرد.
به همین دلیل هنگام اجرای برنامههای زمانبر، تا پایان کار خبری از Prompt جدید نیست.
اما وظایف شل فقط به انتظار کشیدن محدود نمیشود.
اگر کاربر کلیدهای Ctrl+C یا Ctrl+Z را فشار دهد، این شل است که باید سیگنال مناسب را مدیریت کند.
همچنین اگر برنامهای را با
& در پسزمینه اجرا کنیم، باز هم شل مسئول مدیریت آن خواهد بود.او باید:
وضعیت Jobها را نگه دارد.
پایان اجرای آنها را تشخیص دهد.
در صورت نیاز به کاربر اطلاع دهد.
و همزمان Prompt را برای اجرای دستورات بعدی نمایش دهد.
به همین دلیل شل صرفاً یک اجراکنندهی برنامه نیست؛ بلکه مدیر چرخهی اجرای آنها نیز محسوب میشود.
ادامه دارد...
@Amin_TechLab
👾3❤1🔥1
Amin'sTechLab
پشت صحنهی یک دستور در لینوکس! (۹/۱۰) کار شل با اجرای یک برنامه تمام نمیشود. اگر برنامه در Foreground اجرا شده باشد، شل تا پایان اجرای آن منتظر میماند و کنترل ترمینال را دوباره پس میگیرد. به همین دلیل هنگام اجرای برنامههای زمانبر، تا پایان کار خبری از…
پشت صحنهی یک دستور در لینوکس! (۱۰/۱۰)
اگر تمام این مسیر را مرور کنیم، میبینیم پشت یک خط فرمان ساده، دنیای بزرگی پنهان شده است.
ابتدا ترمینال ورودی ما را دریافت میکند.
شل آن را میخواند، تجزیه و تحلیل میکند، متغیرها را گسترش میدهد، مسیر برنامه را پیدا میکند و تصمیم میگیرد چه چیزی باید اجرا شود.
در ادامه، با استفاده از مفاهیمی مانند fork و exec فرآیندهای جدید ایجاد میشوند و پس از پایان اجرا، شل دوباره کنترل ترمینال را در اختیار میگیرد.
از طرف دیگر، شل خودش نیز یک زبان برنامهنویسی کامل است؛ زبانی که امکان تعریف متغیر، شرط، حلقه، توابع و خودکارسازی بسیاری از کارها را فراهم میکند.
شاید به همین دلیل است که با وجود دهها محیط گرافیکی مدرن، هنوز هم خط فرمان یکی از قدرتمندترین ابزارهای تعامل با سیستمعامل محسوب میشود.
شل یک ابزار قدیمی نیست؛ بلکه یکی از بنیادیترین بخشهای دنیای یونیکس است که بعد از چند دهه، همچنان همان سادگی، انعطاف و قدرت روز اول را حفظ کرده است.
پایان.
@Amin_TechLab
اگر تمام این مسیر را مرور کنیم، میبینیم پشت یک خط فرمان ساده، دنیای بزرگی پنهان شده است.
ابتدا ترمینال ورودی ما را دریافت میکند.
شل آن را میخواند، تجزیه و تحلیل میکند، متغیرها را گسترش میدهد، مسیر برنامه را پیدا میکند و تصمیم میگیرد چه چیزی باید اجرا شود.
در ادامه، با استفاده از مفاهیمی مانند fork و exec فرآیندهای جدید ایجاد میشوند و پس از پایان اجرا، شل دوباره کنترل ترمینال را در اختیار میگیرد.
از طرف دیگر، شل خودش نیز یک زبان برنامهنویسی کامل است؛ زبانی که امکان تعریف متغیر، شرط، حلقه، توابع و خودکارسازی بسیاری از کارها را فراهم میکند.
شاید به همین دلیل است که با وجود دهها محیط گرافیکی مدرن، هنوز هم خط فرمان یکی از قدرتمندترین ابزارهای تعامل با سیستمعامل محسوب میشود.
شل یک ابزار قدیمی نیست؛ بلکه یکی از بنیادیترین بخشهای دنیای یونیکس است که بعد از چند دهه، همچنان همان سادگی، انعطاف و قدرت روز اول را حفظ کرده است.
پایان.
@Amin_TechLab
👾3❤1🔥1
خب حالا که ساختار Shell در لینوکس رو باهاش آشنا شدیم .
وقتی خوبیه که Mini-Shell بنویسیم تا دقیقا عملکردش رو درک کنیم .
در ادامه ابتدا این Mini-Shell را به زبان C مینویسیم و بعد همان را با زبان Rust بازنویسی میکنیم .
@Amin_TechLab
وقتی خوبیه که Mini-Shell بنویسیم تا دقیقا عملکردش رو درک کنیم .
در ادامه ابتدا این Mini-Shell را به زبان C مینویسیم و بعد همان را با زبان Rust بازنویسی میکنیم .
@Amin_TechLab
👾2❤1🔥1
💻 ساخت یک Mini Shell در کمتر از ۵۰ خط کد!
اگر بخواهیم سادهترین نسخهی یک Shell را بنویسیم، فقط به چند قابلیت نیاز داریم:
دریافت یک دستور از کاربر
ساخت یک Process جدید (
اجرای برنامه (
منتظر ماندن تا پایان اجرای برنامه (
کد زیر دقیقاً همین کار را انجام میدهد.
این کد چگونه کار میکند؟
🔹
یک خط از کاربر دریافت میکند.
🔹
رشته را بر اساس فاصله به آرگومانهای مختلف تقسیم میکند.
برای مثال:
به این تبدیل میشود:
🔹
از فرآیند فعلی یک نسخهی جدید ایجاد میکند.
بعد از این تابع دو Process وجود دارد:
Parent (شل)
Child (برنامهای که قرار است اجرا شود)
🔹
فرآیند فرزند را با برنامهی موردنظر جایگزین میکند.
اگر بنویسید:
در واقع Child دیگر Mini Shell نیست؛ تبدیل به برنامهی
🔹
شل اصلی منتظر میماند تا اجرای برنامه تمام شود و سپس دوباره Prompt را نمایش میدهد.
این دقیقاً همان چرخهای است که تقریباً تمام Shellهای لینوکس (مانند Bash و Zsh) برای اجرای برنامههای خارجی انجام میدهند:
این نمونه فقط یک Shell آموزشی است و قابلیتهایی مثل Pipeline (
@Amin_TechLab
اگر بخواهیم سادهترین نسخهی یک Shell را بنویسیم، فقط به چند قابلیت نیاز داریم:
دریافت یک دستور از کاربر
ساخت یک Process جدید (
fork)اجرای برنامه (
execvp)منتظر ماندن تا پایان اجرای برنامه (
waitpid)کد زیر دقیقاً همین کار را انجام میدهد.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
char line[256];
while (1)
{
printf("mini-shell> ");
if (!fgets(line, sizeof(line), stdin))
break;
line[strcspn(line, "\n")] = '\0';
if (strcmp(line, "exit") == 0)
break;
char *argv[32];
int argc = 0;
char *token = strtok(line, " ");
while (token && argc < 31)
{
argv[argc++] = token;
token = strtok(NULL, " ");
}
argv[argc] = NULL;
pid_t pid = fork();
if (pid == 0)
{
execvp(argv[0], argv);
perror("execvp");
exit(EXIT_FAILURE);
}
else
{
waitpid(pid, NULL, 0);
}
}
return 0;
}
این کد چگونه کار میکند؟
🔹
fgets()یک خط از کاربر دریافت میکند.
🔹
strtok()رشته را بر اساس فاصله به آرگومانهای مختلف تقسیم میکند.
برای مثال:
ls -l /home
به این تبدیل میشود:
argv[0] = "ls"
argv[1] = "-l"
argv[2] = "/home"
🔹
fork()از فرآیند فعلی یک نسخهی جدید ایجاد میکند.
بعد از این تابع دو Process وجود دارد:
Parent (شل)
Child (برنامهای که قرار است اجرا شود)
🔹
execvp()فرآیند فرزند را با برنامهی موردنظر جایگزین میکند.
اگر بنویسید:
ls -l
در واقع Child دیگر Mini Shell نیست؛ تبدیل به برنامهی
ls میشود.🔹
waitpid()شل اصلی منتظر میماند تا اجرای برنامه تمام شود و سپس دوباره Prompt را نمایش میدهد.
این دقیقاً همان چرخهای است که تقریباً تمام Shellهای لینوکس (مانند Bash و Zsh) برای اجرای برنامههای خارجی انجام میدهند:
Read Command
│
▼
Parse Arguments
│
▼
fork()
│
▼
execvp()
│
▼
Program Runs
│
▼
waitpid()
│
▼
Show Prompt Again
این نمونه فقط یک Shell آموزشی است و قابلیتهایی مثل Pipeline (
|)، Redirect (>)، متغیرهای محیطی، Wildcard (*)، Job Control، Builtins و Quote Parsing را پیادهسازی نمیکند؛ اما برای درک نحوهی اجرای دستورات در لینوکس، یکی از بهترین نقطههای شروع محسوب میشود.@Amin_TechLab
❤2👾2🔥1
💻حالا همان Mini Shell که در پست قبل با زبان C نوشتیم را با Rust بازنویسی میکنیم.
جالب است بدانید که در Rust دیگر خبری از
کد زیر دقیقاً همان Shell سادهی پست قبل را پیادهسازی میکند:
این برنامه چگونه کار میکند؟
🔹 دریافت ورودی
ابتدا یک خط از کاربر خوانده میشود:
🔹 تجزیه دستور
مانند نسخهی C، ورودی بر اساس فاصله جدا میشود:
اگر کاربر بنویسد:
بردار
🔹 اجرای برنامه
در این قسمت تفاوت اصلی Rust و C دیده میشود:
کافی است نام برنامه و آرگومانها را مشخص کنیم.
کتابخانهی استاندارد Rust در سیستمهای شبهیونیکس خودش در پشت صحنه عملیات fork → exec → wait را انجام میدهد و نتیجهی اجرای برنامه را به ما برمیگرداند.
به عبارت دیگر، ما همان رفتار Shell را داریم، اما بدون اینکه مستقیماً با
چرا این کد اینقدر کوتاهتر است؟
در نسخهی C مجبور بودیم:
حافظه را مدیریت کنیم.
رشته را Tokenize کنیم.
خطاها را مدیریت کنیم.
با
اما در Rust، بیشتر این پیچیدگیها داخل
آیا Rust واقعاً از fork استفاده نمیکند؟
یکی از سؤالات رایج همین است.
خیر؛ این فقط ظاهر ماجراست.
در لینوکس، زمانی که از
بنابراین Rust چیزی را جایگزین سیستمعامل نکرده است؛ فقط یک Abstraction سطح بالا روی همان APIهای قدیمی یونیکس ساخته تا توسعهدهنده با کد کمتر، خواناتر و ایمنتر همان نتیجه را به دست آورد.
به همین دلیل اگر مفاهیم
@Amin_TechLab
جالب است بدانید که در Rust دیگر خبری از
fork() و execvp() نیست. کتابخانهی استاندارد Rust این جزئیات سطح پایین را پشت یک API ساده مخفی کرده و ما فقط با std::process::Command کار میکنیم.کد زیر دقیقاً همان Shell سادهی پست قبل را پیادهسازی میکند:
use std::io::{self, Write};
use std::process::Command;
fn main() {
loop {
print!("mini-shell> ");
io::stdout().flush().unwrap();
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
let line = line.trim();
if line.is_empty() {
continue;
}
if line == "exit" {
break;
}
let args: Vec<&str> = line.split_whitespace().collect();
let status = Command::new(args[0])
.args(&args[1..])
.status();
match status {
Ok(status) => {
println!("Exit Status: {}", status);
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
}
}این برنامه چگونه کار میکند؟
🔹 دریافت ورودی
ابتدا یک خط از کاربر خوانده میشود:
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
🔹 تجزیه دستور
مانند نسخهی C، ورودی بر اساس فاصله جدا میشود:
let args: Vec<&str> =
line.split_whitespace().collect();
اگر کاربر بنویسد:
ls -l /home
بردار
args به این شکل خواهد بود:["ls", "-l", "/home"]
🔹 اجرای برنامه
در این قسمت تفاوت اصلی Rust و C دیده میشود:
Command::new(args[0])
.args(&args[1..])
.status();
کافی است نام برنامه و آرگومانها را مشخص کنیم.
کتابخانهی استاندارد Rust در سیستمهای شبهیونیکس خودش در پشت صحنه عملیات fork → exec → wait را انجام میدهد و نتیجهی اجرای برنامه را به ما برمیگرداند.
به عبارت دیگر، ما همان رفتار Shell را داریم، اما بدون اینکه مستقیماً با
fork() یا execvp() کار کنیم.چرا این کد اینقدر کوتاهتر است؟
در نسخهی C مجبور بودیم:
حافظه را مدیریت کنیم.
رشته را Tokenize کنیم.
fork() را صدا بزنیم.execvp() را اجرا کنیم.خطاها را مدیریت کنیم.
با
waitpid() منتظر پایان فرآیند بمانیم.اما در Rust، بیشتر این پیچیدگیها داخل
std::process::Command پنهان شدهاند و API سادهتری در اختیار برنامهنویس قرار میگیرد.آیا Rust واقعاً از fork استفاده نمیکند؟
یکی از سؤالات رایج همین است.
خیر؛ این فقط ظاهر ماجراست.
در لینوکس، زمانی که از
Command::status() یا Command::spawn() استفاده میکنید، Rust در نهایت از مکانیزمهای سیستمعامل برای ایجاد فرآیند جدید استفاده میکند. بسته به سیستمعامل و نسخهی آن، این کار ممکن است با fork()، vfork()، posix_spawn() یا مکانیزمهای مشابه انجام شود.بنابراین Rust چیزی را جایگزین سیستمعامل نکرده است؛ فقط یک Abstraction سطح بالا روی همان APIهای قدیمی یونیکس ساخته تا توسعهدهنده با کد کمتر، خواناتر و ایمنتر همان نتیجه را به دست آورد.
به همین دلیل اگر مفاهیم
fork، exec و wait را در C یاد بگیرید، درک رفتار Command در Rust نیز بسیار سادهتر خواهد بود.@Amin_TechLab
🔥2👾2❤1
💻 یکی از خطرناکترین مفاهیم در زبان C: Sequence Point و Unsequenced Modifications (بخش اول)
اگر سالها با زبان C کار کرده باشید، احتمالاً حداقل یک بار با کدی مواجه شدهاید که روی یک کامپایلر درست کار میکند، اما روی کامپایلر یا سطح Optimization دیگر، نتیجه متفاوتی میدهد.
در بسیاری از موارد، دلیل این رفتار یک مفهوم مهم در استاندارد C است:
این مفهوم یکی از رایجترین دلایل Undefined Behavior در زبان C محسوب میشود.
فرض کنید کد زیر را بنویسید:
به نظر شما خروجی چیست؟
پاسخ صحیح این است:
هیچ پاسخ درستی وجود ندارد.
طبق استاندارد C، این کد Undefined Behavior است.
چرا؟
بیایید عبارت زیر را بررسی کنیم:
در این عبارت، متغیر
و
اما استاندارد C مشخص نمیکند که کدامیک باید ابتدا اجرا شود.
کامپایلر میتواند:
ابتدا
ابتدا
حتی بخشی از هر دو را همزمان بهینهسازی کند.
هیچ ترتیب مشخصی وجود ندارد.
Sequence Point چیست؟
در زبان C، بعضی نقاط وجود دارند که استاندارد تضمین میکند تمام عملیات قبلی کامل شدهاند و سپس اجرای بخش بعدی آغاز میشود.
به این نقاط میگویند:
Sequence Point
در استانداردهای جدید C بیشتر از اصطلاحهای Sequenced Before و Unsequenced استفاده میشود، اما هنوز هم بسیاری از منابع آموزشی از عبارت Sequence Point استفاده میکنند.
چه زمانی مشکل ایجاد میشود؟
اگر در یک عبارت:
یک متغیر را بیش از یک بار تغییر دهید.
یا آن را همزمان بخوانید و تغییر دهید.
بدون اینکه ترتیب اجرای این عملیات مشخص باشد.
برنامه وارد Undefined Behavior میشود.
این یعنی استاندارد C دیگر هیچ تضمینی درباره نتیجه اجرای برنامه نمیدهد.
در بخش دوم، چند مثال واقعی دیگر را بررسی میکنیم و خواهیم دید چرا این دسته از باگها فقط با تغییر کامپایلر یا فعال کردن Optimization ظاهر میشوند.
@Amin_TechLab
اگر سالها با زبان C کار کرده باشید، احتمالاً حداقل یک بار با کدی مواجه شدهاید که روی یک کامپایلر درست کار میکند، اما روی کامپایلر یا سطح Optimization دیگر، نتیجه متفاوتی میدهد.
در بسیاری از موارد، دلیل این رفتار یک مفهوم مهم در استاندارد C است:
Unsequenced Modifications
این مفهوم یکی از رایجترین دلایل Undefined Behavior در زبان C محسوب میشود.
فرض کنید کد زیر را بنویسید:
#include <stdio.h>
int main()
{
int i = 5;
printf("%d\n", i++ + ++i);
return 0;
}
به نظر شما خروجی چیست؟
12 ؟
13 ؟
14 ؟
پاسخ صحیح این است:
هیچ پاسخ درستی وجود ندارد.
طبق استاندارد C، این کد Undefined Behavior است.
چرا؟
بیایید عبارت زیر را بررسی کنیم:
i++ + ++i
در این عبارت، متغیر
i دو بار تغییر میکند:i++
و
++i
اما استاندارد C مشخص نمیکند که کدامیک باید ابتدا اجرا شود.
کامپایلر میتواند:
ابتدا
i++ را اجرا کند.ابتدا
++i را اجرا کند.حتی بخشی از هر دو را همزمان بهینهسازی کند.
هیچ ترتیب مشخصی وجود ندارد.
Sequence Point چیست؟
در زبان C، بعضی نقاط وجود دارند که استاندارد تضمین میکند تمام عملیات قبلی کامل شدهاند و سپس اجرای بخش بعدی آغاز میشود.
به این نقاط میگویند:
Sequence Point
در استانداردهای جدید C بیشتر از اصطلاحهای Sequenced Before و Unsequenced استفاده میشود، اما هنوز هم بسیاری از منابع آموزشی از عبارت Sequence Point استفاده میکنند.
چه زمانی مشکل ایجاد میشود؟
اگر در یک عبارت:
یک متغیر را بیش از یک بار تغییر دهید.
یا آن را همزمان بخوانید و تغییر دهید.
بدون اینکه ترتیب اجرای این عملیات مشخص باشد.
برنامه وارد Undefined Behavior میشود.
این یعنی استاندارد C دیگر هیچ تضمینی درباره نتیجه اجرای برنامه نمیدهد.
در بخش دوم، چند مثال واقعی دیگر را بررسی میکنیم و خواهیم دید چرا این دسته از باگها فقط با تغییر کامپایلر یا فعال کردن Optimization ظاهر میشوند.
@Amin_TechLab
❤4🔥2👾1
Amin'sTechLab
💻 یکی از خطرناکترین مفاهیم در زبان C: Sequence Point و Unsequenced Modifications (بخش اول) اگر سالها با زبان C کار کرده باشید، احتمالاً حداقل یک بار با کدی مواجه شدهاید که روی یک کامپایلر درست کار میکند، اما روی کامپایلر یا سطح Optimization دیگر، نتیجه…
💻 یکی از خطرناکترین مفاهیم در زبان C: Sequence Point و Unsequenced Modifications (بخش دوم)
در بخش قبل دیدیم که اگر یک متغیر در یک عبارت چند بار تغییر کند و ترتیب اجرای این تغییرات مشخص نباشد، برنامه وارد Undefined Behavior میشود.
حالا چند مثال دیگر را بررسی کنیم.
مثال اول:
بسیاری تصور میکنند مقدار
اما این کد نیز Undefined Behavior است.
مثال دوم:
به نظر شما خروجی چیست؟
یا
یا حتی
واقعیت این است که استاندارد C هیچکدام از این خروجیها را تضمین نمیکند.
چون ترتیب ارزیابی آرگومانهای یک تابع در زبان C مشخص نشده است.
مثال سوم:
در این مثال نیز مقدار
نتیجه؟
باز هم Undefined Behavior.
چرا کامپایلر این کدها را خطا نمیگیرد؟
زیرا استاندارد C فقط مشخص میکند که رفتار این برنامه تعریفنشده است؛ اما کامپایلر را مجبور نمیکند که حتماً برای آن خطا یا حتی Warning صادر کند.
کامپایلر فرض میکند شما قوانین زبان را رعایت کردهاید و بر همان اساس کد را بهینه میکند.
به همین دلیل ممکن است:
یک خروجی مشخص تولید کند.
اما همان برنامه با:
یا حتی روی کامپایلر دیگری، رفتار کاملاً متفاوتی داشته باشد.
چگونه از این مشکل جلوگیری کنیم؟
به جای نوشتن عبارتهای پیچیده مانند:
کد را به چند مرحله ساده تقسیم کنید:
نوشتن چند خط کد بیشتر، همیشه بهتر از تکیه بر رفتاری است که استاندارد C آن را تضمین نمیکند.
جمعبندی
یکی از مهمترین قوانین زبان C این است:
شاید این نوع کدها امروز روی سیستم شما درست اجرا شوند، اما هیچ تضمینی وجود ندارد که فردا، با یک نسخه جدید از کامپایلر یا فقط با فعال کردن Optimization، همان رفتار را حفظ کنند.
به همین دلیل برنامهنویسان حرفهای C و Embedded از نوشتن عبارتهای پیچیدهای که همزمان یک متغیر را چند بار تغییر میدهند، خودداری میکنند.
در زبان C، ساده نوشتن فقط خوانایی را افزایش نمیدهد؛ بلکه از ورود برنامه به یکی از خطرناکترین انواع Undefined Behavior نیز جلوگیری میکند. ⚠️
@Amin_TechLab
در بخش قبل دیدیم که اگر یک متغیر در یک عبارت چند بار تغییر کند و ترتیب اجرای این تغییرات مشخص نباشد، برنامه وارد Undefined Behavior میشود.
حالا چند مثال دیگر را بررسی کنیم.
مثال اول:
int i = 0;
i = i++;
بسیاری تصور میکنند مقدار
i برابر ۱ خواهد شد.اما این کد نیز Undefined Behavior است.
مثال دوم:
int i = 3;
printf("%d %d\n", i++, i++);
به نظر شما خروجی چیست؟
3 4
یا
4 3
یا حتی
4 4
واقعیت این است که استاندارد C هیچکدام از این خروجیها را تضمین نمیکند.
چون ترتیب ارزیابی آرگومانهای یک تابع در زبان C مشخص نشده است.
مثال سوم:
int a = 10;
a = a++ + 5;
در این مثال نیز مقدار
a همزمان خوانده و تغییر داده میشود، بدون اینکه ترتیب اجرای این عملیات مشخص باشد.نتیجه؟
باز هم Undefined Behavior.
چرا کامپایلر این کدها را خطا نمیگیرد؟
زیرا استاندارد C فقط مشخص میکند که رفتار این برنامه تعریفنشده است؛ اما کامپایلر را مجبور نمیکند که حتماً برای آن خطا یا حتی Warning صادر کند.
کامپایلر فرض میکند شما قوانین زبان را رعایت کردهاید و بر همان اساس کد را بهینه میکند.
به همین دلیل ممکن است:
gcc main.c
یک خروجی مشخص تولید کند.
اما همان برنامه با:
gcc main.c -O2
یا حتی روی کامپایلر دیگری، رفتار کاملاً متفاوتی داشته باشد.
چگونه از این مشکل جلوگیری کنیم؟
به جای نوشتن عبارتهای پیچیده مانند:
printf("%d\n", i++ + ++i);کد را به چند مرحله ساده تقسیم کنید:
int a = i;
i++;
int b = i;
i++;
printf("%d\n", a + b);
نوشتن چند خط کد بیشتر، همیشه بهتر از تکیه بر رفتاری است که استاندارد C آن را تضمین نمیکند.
جمعبندی
یکی از مهمترین قوانین زبان C این است:
هرگز یک متغیر را بیش از یک بار در یک عبارت تغییر ندهید، مگر اینکه ترتیب اجرای تمام آن عملیات توسط استاندارد C تضمین شده باشد.
شاید این نوع کدها امروز روی سیستم شما درست اجرا شوند، اما هیچ تضمینی وجود ندارد که فردا، با یک نسخه جدید از کامپایلر یا فقط با فعال کردن Optimization، همان رفتار را حفظ کنند.
به همین دلیل برنامهنویسان حرفهای C و Embedded از نوشتن عبارتهای پیچیدهای که همزمان یک متغیر را چند بار تغییر میدهند، خودداری میکنند.
در زبان C، ساده نوشتن فقط خوانایی را افزایش نمیدهد؛ بلکه از ورود برنامه به یکی از خطرناکترین انواع Undefined Behavior نیز جلوگیری میکند. ⚠️
@Amin_TechLab
👍3❤2🔥2👾2
🚀 بخش اول | آیا وقت بازنویسی PostgreSQL رسیده است؟
سالهاست PostgreSQL به عنوان یکی از قدرتمندترین دیتابیسهای متنباز شناخته میشود؛ اما معماری آن متعلق به دههها قبل است.
تیم Turso حالا پروژهای جاهطلبانه را آغاز کرده است:
ساخت یک نسخه مدرن از PostgreSQL با زبان Rust، اما نه با یک Rewrite ساده!
ایده اصلی این است که هسته Rust-based تورسو به یک "LLVM برای دیتابیسها" تبدیل شود؛ یعنی یک Core مشترک که موتورهای مختلف دیتابیس بتوانند روی آن پیادهسازی شوند.
در این معماری:
✅ PostgreSQL فقط یک Frontend خواهد بود.
✅ SQLite اولین Frontend این هسته است.
✅ در آینده حتی موتورهای دیگری نیز میتوانند روی همین زیرساخت ساخته شوند.
هدف فقط سازگاری با PostgreSQL نیست؛ بلکه ارائه معماریای مدرنتر با قابلیتهایی مانند:
اجرای Async واقعی
بهرهگیری از ایمنی حافظه Rust
اجرای Embedded و Browser Native
حذف محدودیتهای معماری قدیمی PostgreSQL
امکان توسعه سادهتر قابلیتهای جدید
به بیان دیگر، این پروژه میخواهد PostgreSQL را برای نسل بعدی نرمافزارها بازتعریف کند، نه اینکه صرفاً آن را به Rust ترجمه کند.
@Amin_Techlab
سالهاست PostgreSQL به عنوان یکی از قدرتمندترین دیتابیسهای متنباز شناخته میشود؛ اما معماری آن متعلق به دههها قبل است.
تیم Turso حالا پروژهای جاهطلبانه را آغاز کرده است:
ساخت یک نسخه مدرن از PostgreSQL با زبان Rust، اما نه با یک Rewrite ساده!
ایده اصلی این است که هسته Rust-based تورسو به یک "LLVM برای دیتابیسها" تبدیل شود؛ یعنی یک Core مشترک که موتورهای مختلف دیتابیس بتوانند روی آن پیادهسازی شوند.
در این معماری:
✅ PostgreSQL فقط یک Frontend خواهد بود.
✅ SQLite اولین Frontend این هسته است.
✅ در آینده حتی موتورهای دیگری نیز میتوانند روی همین زیرساخت ساخته شوند.
هدف فقط سازگاری با PostgreSQL نیست؛ بلکه ارائه معماریای مدرنتر با قابلیتهایی مانند:
اجرای Async واقعی
بهرهگیری از ایمنی حافظه Rust
اجرای Embedded و Browser Native
حذف محدودیتهای معماری قدیمی PostgreSQL
امکان توسعه سادهتر قابلیتهای جدید
به بیان دیگر، این پروژه میخواهد PostgreSQL را برای نسل بعدی نرمافزارها بازتعریف کند، نه اینکه صرفاً آن را به Rust ترجمه کند.
@Amin_Techlab
👾2🔥1
Amin'sTechLab
🚀 بخش اول | آیا وقت بازنویسی PostgreSQL رسیده است؟ سالهاست PostgreSQL به عنوان یکی از قدرتمندترین دیتابیسهای متنباز شناخته میشود؛ اما معماری آن متعلق به دههها قبل است. تیم Turso حالا پروژهای جاهطلبانه را آغاز کرده است: ساخت یک نسخه مدرن از PostgreSQL…
🔬 بخش دوم | معماری فنی؛ PostgreSQL روی یک Runtime جدید
اگر تصور میکنید Turso در حال Rewrite کردن PostgreSQL با Rust است، در واقع داستان بسیار جذابتر از این است.
ایده اصلی، جداسازی رابط PostgreSQL از موتور اجرای دیتابیس است.
در معماری فعلی PostgreSQL تقریباً تمام اجزای سیستم—از Parser و Planner گرفته تا Executor، Storage Engine، Buffer Manager و MVCC—بهشدت به یکدیگر وابسته هستند. همین وابستگی باعث شده توسعه قابلیتهای جدید، اجرای Async، استفاده در Edge و WebAssembly یا حتی تغییرات اساسی در Storage بسیار دشوار باشد.
تیم Turso این وابستگی را میشکند.
معماری پیشنهادی به این صورت است:
به جای اجرای مستقیم Queryها در Executor سنتی PostgreSQL، دستورات SQL پس از Parse شدن به یک Intermediate Representation (IR) یا Bytecode تبدیل میشوند و سپس توسط Runtime موتور Limbo اجرا خواهند شد.
این دقیقاً همان ایدهای است که LLVM در دنیای کامپایلرها پیادهسازی کرد؛ یعنی یک لایه میانی که چندین Frontend و Backend میتوانند از آن استفاده کنند.
مزیتهای این معماری عبارتاند از:
✅ Memory Safety
تمام هسته سیستم با Rust توسعه داده میشود؛ بنابراین بخش بزرگی از باگهای رایج C مانند Buffer Overflow، Use-after-Free و Data Race حذف میشوند.
✅ Storage Engine مستقل
Storage دیگر محدود به معماری قدیمی PostgreSQL نیست و میتواند برای SSD، Edge Computing و Object Storage بهینه شود.
✅ Async Runtime واقعی
برخلاف PostgreSQL که مدل Process-per-Connection دارد، Runtime جدید میتواند بر پایه Async I/O و Event Loop اجرا شود؛ موضوعی که مصرف حافظه و تعداد Connectionهای همزمان را بهبود میدهد.
✅ ماژولار شدن اجزای دیتابیس
Planner، Executor، Storage Engine و Replication به صورت مؤلفههای مستقل طراحی میشوند؛ بنابراین اضافه کردن قابلیتهایی مانند Vector Search، Columnar Storage یا موتورهای Query جدید بسیار سادهتر خواهد بود.
✅ Embedded و WebAssembly
از آنجا که Limbo یک Runtime سبک مبتنی بر Rust است، امکان اجرای دیتابیس داخل Browser (WASM)، Edge Functionها و حتی اپلیکیشنهای Local-first فراهم میشود؛ قابلیتی که با PostgreSQL کلاسیک تقریباً غیرممکن است.
نکته مهم اینجاست که هدف Turso جایگزین کردن اکوسیستم PostgreSQL نیست؛ بلکه حفظ سازگاری کامل با پروتکل شبکه، Dialect زبان SQL و ابزارهای موجود (مانند pgAdmin، libpq، ORMها و Driverهای PostgreSQL) است، در حالی که موتور اجرا کاملاً مدرن و بازطراحی شده خواهد بود.
اگر این معماری به بلوغ برسد، احتمالاً برای اولین بار شاهد جدایی واقعی بین PostgreSQL بهعنوان Interface و PostgreSQL بهعنوان Database Engine خواهیم بود؛ تغییری که میتواند آینده دیتابیسهای رابطهای را مانند نقش LLVM در دنیای کامپایلرها متحول کند.
@Amin_Techlab
اگر تصور میکنید Turso در حال Rewrite کردن PostgreSQL با Rust است، در واقع داستان بسیار جذابتر از این است.
ایده اصلی، جداسازی رابط PostgreSQL از موتور اجرای دیتابیس است.
در معماری فعلی PostgreSQL تقریباً تمام اجزای سیستم—از Parser و Planner گرفته تا Executor، Storage Engine، Buffer Manager و MVCC—بهشدت به یکدیگر وابسته هستند. همین وابستگی باعث شده توسعه قابلیتهای جدید، اجرای Async، استفاده در Edge و WebAssembly یا حتی تغییرات اساسی در Storage بسیار دشوار باشد.
تیم Turso این وابستگی را میشکند.
معماری پیشنهادی به این صورت است:
PostgreSQL Wire Protocol
│
SQL Parser
│
PostgreSQL AST
│
Query Planner/Translator
│
Turso IR / Bytecode
│
Limbo Runtime Engine
│
Storage + MVCC + Replication
به جای اجرای مستقیم Queryها در Executor سنتی PostgreSQL، دستورات SQL پس از Parse شدن به یک Intermediate Representation (IR) یا Bytecode تبدیل میشوند و سپس توسط Runtime موتور Limbo اجرا خواهند شد.
این دقیقاً همان ایدهای است که LLVM در دنیای کامپایلرها پیادهسازی کرد؛ یعنی یک لایه میانی که چندین Frontend و Backend میتوانند از آن استفاده کنند.
مزیتهای این معماری عبارتاند از:
✅ Memory Safety
تمام هسته سیستم با Rust توسعه داده میشود؛ بنابراین بخش بزرگی از باگهای رایج C مانند Buffer Overflow، Use-after-Free و Data Race حذف میشوند.
✅ Storage Engine مستقل
Storage دیگر محدود به معماری قدیمی PostgreSQL نیست و میتواند برای SSD، Edge Computing و Object Storage بهینه شود.
✅ Async Runtime واقعی
برخلاف PostgreSQL که مدل Process-per-Connection دارد، Runtime جدید میتواند بر پایه Async I/O و Event Loop اجرا شود؛ موضوعی که مصرف حافظه و تعداد Connectionهای همزمان را بهبود میدهد.
✅ ماژولار شدن اجزای دیتابیس
Planner، Executor، Storage Engine و Replication به صورت مؤلفههای مستقل طراحی میشوند؛ بنابراین اضافه کردن قابلیتهایی مانند Vector Search، Columnar Storage یا موتورهای Query جدید بسیار سادهتر خواهد بود.
✅ Embedded و WebAssembly
از آنجا که Limbo یک Runtime سبک مبتنی بر Rust است، امکان اجرای دیتابیس داخل Browser (WASM)، Edge Functionها و حتی اپلیکیشنهای Local-first فراهم میشود؛ قابلیتی که با PostgreSQL کلاسیک تقریباً غیرممکن است.
نکته مهم اینجاست که هدف Turso جایگزین کردن اکوسیستم PostgreSQL نیست؛ بلکه حفظ سازگاری کامل با پروتکل شبکه، Dialect زبان SQL و ابزارهای موجود (مانند pgAdmin، libpq، ORMها و Driverهای PostgreSQL) است، در حالی که موتور اجرا کاملاً مدرن و بازطراحی شده خواهد بود.
اگر این معماری به بلوغ برسد، احتمالاً برای اولین بار شاهد جدایی واقعی بین PostgreSQL بهعنوان Interface و PostgreSQL بهعنوان Database Engine خواهیم بود؛ تغییری که میتواند آینده دیتابیسهای رابطهای را مانند نقش LLVM در دنیای کامپایلرها متحول کند.
@Amin_Techlab
👾2🔥1
Harmonic Firmware Initiative (HFI) aims to create, standardize, and maintain — for RISC-V systems — something the x86 world has taken for granted for forty years: a genuine power-on firmware experience. You switch the machine on and it presents itself on its own display — a firmware identity and POST screen, hardware enumeration, a Set-Up configuration editor, and boot selection — the way every PC has since the 1980s, and the way no bare RISC-V board does today.
@Amin_Techlab
@Amin_Techlab
👍1🔥1👾1
HFI-WhitePaper.pdf
215.8 KB
Harmonic Firmware Initiative (HFI) aims to create, standardize, and maintain — for RISC-V systems — something the x86 world has taken for granted for forty years: a genuine power-on firmware experience
@Amin_Techlab
@Amin_Techlab
👍1🔥1👾1