یه specifier که تو استاندارد c++11 معرفی شده به نام constant expressions که بصورت تو کد نوشته میشه، توی مبحث بعد کتاب این را بررسی کرده حالا چرا؟ در ادامه توضیح میدم.
اول اینکه چه فرقی با specifier زیر داره؟ جفتشون یک literal را از نوع read-only تعریف میکنند.
اگر یه flash back کوتاه به c99 بزنیم، اونجا برای تعریف مقادیر ثابت از طریق macro عمل میکردیم به صورت زیر:
یه مشکلی اساسی که داشت این بود که data-type متغیر مشخص نبود و گاهی اوقات با یک خورده بی دقتی باعث بهم خوردگی مقادیر میشد مخصوصا وقتایی که قرار بود مقدار تو یه عبارا ریاضی با data-type مختلف قرار بگیره.
این مشکل با معرفی constexpr تو استاندارد c++11 حل شد به روشی که توضیح دادم.
@geek_ops
constexpr double pi = 3.14159265359;
اول اینکه چه فرقی با specifier زیر داره؟ جفتشون یک literal را از نوع read-only تعریف میکنند.
const double pi = 3.14159265359;
اگر یه flash back کوتاه به c99 بزنیم، اونجا برای تعریف مقادیر ثابت از طریق macro عمل میکردیم به صورت زیر:
#define PI 3.14159265359
یه مشکلی اساسی که داشت این بود که data-type متغیر مشخص نبود و گاهی اوقات با یک خورده بی دقتی باعث بهم خوردگی مقادیر میشد مخصوصا وقتایی که قرار بود مقدار تو یه عبارا ریاضی با data-type مختلف قرار بگیره.
این مشکل با معرفی constexpr تو استاندارد c++11 حل شد به روشی که توضیح دادم.
@geek_ops
اگر یه سر به استاندارد در موردش بزنیم، توی ISO/IEC N3142 بخش 5.19 این عبارت را معرفی کرده، جمله ی اولش جالبه:
تو عبارت اشاره کرده به certain contexts یعنی هرجا نه، یه سری جاهای خاص.
ساده تر بخوام بگم یعنی اینکه گاهی اوقات ما برای verify کردن یه عبارت تو کد نیاز به یه سری قانون های اضافه داریم. نیازه که بیشتر یه عبارت بررسی بشه.
توی قسمت 5.9 مفصل در موردش صحبت کرده که اگر همش را بخوام بگم پست از حوصله خارج میشه، علاقه مند بود فایل استاندارد را گذاشتم مطالعه کنید.
@geek_ops
Certain contexts require expressions that satisfy additional requirements as detailed in this sub-clause.
تو عبارت اشاره کرده به certain contexts یعنی هرجا نه، یه سری جاهای خاص.
ساده تر بخوام بگم یعنی اینکه گاهی اوقات ما برای verify کردن یه عبارت تو کد نیاز به یه سری قانون های اضافه داریم. نیازه که بیشتر یه عبارت بررسی بشه.
توی قسمت 5.9 مفصل در موردش صحبت کرده که اگر همش را بخوام بگم پست از حوصله خارج میشه، علاقه مند بود فایل استاندارد را گذاشتم مطالعه کنید.
@geek_ops
یه موردی دیگه ای که باید اشاره کنم اینه که تاییدیه و بررسی یه عبارت را در هنگام translation یا به عبارت دیگه compile-time بررسی میکنه.
این قطعه کد را نگاه کنید، آیا به نظر شما مشکلی داره؟ تو زمان Compile خطا یا هشداری دریافت میکنیم؟
جواب این مشکل دارد ولی هیچ خطا یا هشداری چه در اجرای کد، چه حین زمان Compile دریافت نمیکنیم🤔
حالا برگردیم به سوال اولی که پرسیدم چرا کتاب باید این specifier را توضیح کنه، چون تو سیستم های embedded بدلیل کمبود منابع و حیاتی بودن کد نیازه که یه سری قواعد و قوانین تو تایید یه عبارت بزاریم کدی در بالا نوشتم اینه که یه آدرس memory تو حافظه read-only تعریف شده ولی توی حین اجرای کد این نقض خواهد شد و در حلقه ی for مقدار value عوض میشه.
اینجاست constexpr میتوانه حلال مشکلات باشه اگر const را به constexpr تغییر بدهم میبینید که کد اصلا Compile نخواهد شد.
خطای اینکه عبارت که تعریف کرده یه تابع با خروجی غیر constant را داره call میکنه.
@geek_ops
for (size_t i = 0; i < 5; i++)
{
const int value = rand() % 10;
int result = value + i;
std::cout << "Your value: " << result << '\n';
}
این قطعه کد را نگاه کنید، آیا به نظر شما مشکلی داره؟ تو زمان Compile خطا یا هشداری دریافت میکنیم؟
جواب این مشکل دارد ولی هیچ خطا یا هشداری چه در اجرای کد، چه حین زمان Compile دریافت نمیکنیم🤔
حالا برگردیم به سوال اولی که پرسیدم چرا کتاب باید این specifier را توضیح کنه، چون تو سیستم های embedded بدلیل کمبود منابع و حیاتی بودن کد نیازه که یه سری قواعد و قوانین تو تایید یه عبارت بزاریم کدی در بالا نوشتم اینه که یه آدرس memory تو حافظه read-only تعریف شده ولی توی حین اجرای کد این نقض خواهد شد و در حلقه ی for مقدار value عوض میشه.
اینجاست constexpr میتوانه حلال مشکلات باشه اگر const را به constexpr تغییر بدهم میبینید که کد اصلا Compile نخواهد شد.
خطای اینکه عبارت که تعریف کرده یه تابع با خروجی غیر constant را داره call میکنه.
@geek_ops
مثلا یه خطای خیلی فاحش زیر را ببینید:
چیکار میکنه؟ تو macro تعریف شده، تقسیم بر هم ذخیره میشوند تو یه read-only float و خروجی ذخیره شده را با دو رقم اعشار پرینت میکنیم با gcc کامپایل کنیم خروجی میشه 3.00😱
همین خطای ساده میتوانه در سیستم های حیاتی و real-time منجر به خرابی سیستم، از دست رفتن کل سیستم و ... شود.
اگه به فرم بالا بنویسیم خطای محاسباتی برطرف میشه و خروجی درست یعنی 3.30 نمایش داده میشود.
یه نکته هم همینجا ذکر کنم این توضیحات قرار نیست این را استناد کنه زبان c مشکل داره اینطور نیست تمام این مشکلات با همان زبان c قابل برطرف شدن هست، صرفا میخوایم ویژگی های زبان c++ را توضیح بدهیم که چگونه مشکلاتی که وجود داشته را حل کرده و بتوانیم کد های بهتر و بهینه تری را بنويسيم.
@geek_ops
#include <stdio.h>
#include <stdlib.h>
#define VOLTAGE 3300
#define CURRENT 1000
int main()
{
const float resistance = VOLTAGE / CURRENT;
printf("resistance: %.2f\n", resistance);
return 0;
}
چیکار میکنه؟ تو macro تعریف شده، تقسیم بر هم ذخیره میشوند تو یه read-only float و خروجی ذخیره شده را با دو رقم اعشار پرینت میکنیم با gcc کامپایل کنیم خروجی میشه 3.00😱
همین خطای ساده میتوانه در سیستم های حیاتی و real-time منجر به خرابی سیستم، از دست رفتن کل سیستم و ... شود.
constexpr float VOLTAGE = 3300.0f;
constexpr float CURRENT = 1000.0f;
int main()
{
const float resistance = VOLTAGE / CURRENT;
printf("resistance: %.2f\n", resistance);
return 0;
}
اگه به فرم بالا بنویسیم خطای محاسباتی برطرف میشه و خروجی درست یعنی 3.30 نمایش داده میشود.
یه نکته هم همینجا ذکر کنم این توضیحات قرار نیست این را استناد کنه زبان c مشکل داره اینطور نیست تمام این مشکلات با همان زبان c قابل برطرف شدن هست، صرفا میخوایم ویژگی های زبان c++ را توضیح بدهیم که چگونه مشکلاتی که وجود داشته را حل کرده و بتوانیم کد های بهتر و بهینه تری را بنويسيم.
@geek_ops
یه مثال دیگه هم بزنیم که کامل این مبحث جا بیفته، برای درک بهتر کد را میزارم، بعد arm gcc با flag های پیشفرض Compile میکنم خروجی Assembly را با هم بررسی میکنیم.
خروجی اسمبلی:
خیلی پر حرفی نکنم اینطوره که مقدار رجیستر r7 با offset 4 داخل r3 ذخیره r3 میره تو رجیستر r0 که تو arm برای ذخیره return بعد stack pointer میاد رو تابع main و ادامه کار ...
عملا یه فانکشن ساده چند تا رجیستر فراخوانده و یه سری محاسبات اضافه اگر بیام خروجی square را از نوع constexpr بزارم خروجی را مقایسه کنید:
میبینید تعداد خطوط کد اسمبلی کم تر شد و حجم محاسبات به شدت پایین اومد، عملا داره مقدار square(2) تو زمان Compile محاسبه میکنه و تو رجیستر r3 ذخیره میشود.
@geek_ops
int square(int a)
{
return a*a;
}
int main()
{
int ret = square(2);
return ret;
}
خروجی اسمبلی:
square(int):
push {r7}
sub sp, sp, #12
add r7, sp, #0
str r0, [r7, #4]
ldr r3, [r7, #4]
mul r3, r3, r3
mov r0, r3
adds r7, r7, #12
mov sp, r7
ldr r7, [sp], #4
bx lr
main:
push {r7, lr}
sub sp, sp, #8
add r7, sp, #0
movs r0, #2
bl square(int)
str r0, [r7, #4]
ldr r3, [r7, #4]
mov r0, r3
adds r7, r7, #8
mov sp, r7
pop {r7, pc}
خیلی پر حرفی نکنم اینطوره که مقدار رجیستر r7 با offset 4 داخل r3 ذخیره r3 میره تو رجیستر r0 که تو arm برای ذخیره return بعد stack pointer میاد رو تابع main و ادامه کار ...
عملا یه فانکشن ساده چند تا رجیستر فراخوانده و یه سری محاسبات اضافه اگر بیام خروجی square را از نوع constexpr بزارم خروجی را مقایسه کنید:
main:
push {r7}
sub sp, sp, #12
add r7, sp, #0
movs r3, #4
str r3, [r7, #4]
ldr r3, [r7, #4]
mov r0, r3
adds r7, r7, #12
mov sp, r7
ldr r7, [sp], #4
bx lr
میبینید تعداد خطوط کد اسمبلی کم تر شد و حجم محاسبات به شدت پایین اومد، عملا داره مقدار square(2) تو زمان Compile محاسبه میکنه و تو رجیستر r3 ذخیره میشود.
@geek_ops
توی chapter 1 کتاب دو تا بخش داشت که بخش اول مواردی بود که گفتم سعی کردم با استفاده از منابع مختلف و مثال های اضافه اطلاعات را تکمیل کنم.
ولی بخش دوم برا خودمم خیلی جذاب تر بود، یه مبحث خیلی مهمی هست تو نوشتن کد حالا توی سیستم ها embedded خیلی پر رنگ تر و مهمتر که من تقسیمش میکنم به دو تا قسمت:
1 - code bloat
2 - runtime overhead
اولا اینکه این دو تا مبحث آیا مفهومش یکیه؟؟
فعلا جدا در نظر میگیریم در موردش صحبت میکنیم تا به این نتیجه بررسی چقدر بهم مرتبط هستند و چقدر میشه از هم تفکیک شون کرد.
@geek_ops
ولی بخش دوم برا خودمم خیلی جذاب تر بود، یه مبحث خیلی مهمی هست تو نوشتن کد حالا توی سیستم ها embedded خیلی پر رنگ تر و مهمتر که من تقسیمش میکنم به دو تا قسمت:
1 - code bloat
2 - runtime overhead
اولا اینکه این دو تا مبحث آیا مفهومش یکیه؟؟
فعلا جدا در نظر میگیریم در موردش صحبت میکنیم تا به این نتیجه بررسی چقدر بهم مرتبط هستند و چقدر میشه از هم تفکیک شون کرد.
@geek_ops
Runtime Overhead and Code Bloat
سر بار اضافه یا هر کلمه که مفهومش را برساند، بزارید صحبتم از اینجا شروع کنم اگر یه نجار را در نظر بگیرید که خیلی کار درست و با مهارت بالایی هست در حال ساختن یه تخت خواب خوب نیاز به یه سری وسایل نظیر میخ، دریل و ... داره. اگر شروع به انجام کارش بکنه بعد از انجام عملیات برش شروع کنه قطعات را با پیچ و میخ بهم وصل کردن یه تعداد وسایل کنارش میزاره و شروع میکنه به کار کردن اگر فرض کنیم هر پیچ که توی قطعات چوب باید ببنده یه فاصله مثلا ۲۰ متری تا محل کارش داره، باید بره یه تعداد پیچ بیاره ببنده دوباره همین کار رو انجام بده به زمان صرف شده جهت این رفت و آمد ها runtime overhead میگن یعنی یکسری فعالیت که اضافه که باعث کندی در روند اجرای کار میشه.
حالا اگر سیستمی بخوام صحبت کنم اگر اینطور بخوایم به روند اجرای کد نگاه کنیم، سه قسمتاصلی داشته باشه یکی سیستم، یکی خودمون و یکی هم هر چیزی که بین خودمون و سیستم هست.
سیستم چیکار میکنه هر کال یه فانکشن یه تعداد رجیستر و آرگومان تغییر وضعیت میدهند، push جای دیگه ای. حالا اگر فانکشن پوینتر هم باشه که یکسری کارای اضافه تری، اجرای syscalls, CPU cache داده باید از ram خوانده باشه و کلی کار دیگه.
حالا نقش ما چیه چیز هایی که گفتم را ایجاد میکنیم، میخوای خطا چک بشه، pointer های اضافه انتخاب بد الگوریتم و امثالهم.
و اون چیزی که بینمون هست شامل مواردی کد بهمون تحمیل میکنه شامل constructor, destructor, template و خیلی چیز های دیگه میشه bloatware یا code bloat
در واقع یه مسئلهای که هست گاهی ما یه سری موارد کد را نمی نویسیم ولی تو فایل اجرایی cpu هست. اون این چیز هایی هست که یه خورده باید بیشتر در موردشون صحبت کنیم.
@geek_ops
سر بار اضافه یا هر کلمه که مفهومش را برساند، بزارید صحبتم از اینجا شروع کنم اگر یه نجار را در نظر بگیرید که خیلی کار درست و با مهارت بالایی هست در حال ساختن یه تخت خواب خوب نیاز به یه سری وسایل نظیر میخ، دریل و ... داره. اگر شروع به انجام کارش بکنه بعد از انجام عملیات برش شروع کنه قطعات را با پیچ و میخ بهم وصل کردن یه تعداد وسایل کنارش میزاره و شروع میکنه به کار کردن اگر فرض کنیم هر پیچ که توی قطعات چوب باید ببنده یه فاصله مثلا ۲۰ متری تا محل کارش داره، باید بره یه تعداد پیچ بیاره ببنده دوباره همین کار رو انجام بده به زمان صرف شده جهت این رفت و آمد ها runtime overhead میگن یعنی یکسری فعالیت که اضافه که باعث کندی در روند اجرای کار میشه.
حالا اگر سیستمی بخوام صحبت کنم اگر اینطور بخوایم به روند اجرای کد نگاه کنیم، سه قسمتاصلی داشته باشه یکی سیستم، یکی خودمون و یکی هم هر چیزی که بین خودمون و سیستم هست.
سیستم چیکار میکنه هر کال یه فانکشن یه تعداد رجیستر و آرگومان تغییر وضعیت میدهند، push جای دیگه ای. حالا اگر فانکشن پوینتر هم باشه که یکسری کارای اضافه تری، اجرای syscalls, CPU cache داده باید از ram خوانده باشه و کلی کار دیگه.
حالا نقش ما چیه چیز هایی که گفتم را ایجاد میکنیم، میخوای خطا چک بشه، pointer های اضافه انتخاب بد الگوریتم و امثالهم.
و اون چیزی که بینمون هست شامل مواردی کد بهمون تحمیل میکنه شامل constructor, destructor, template و خیلی چیز های دیگه میشه bloatware یا code bloat
در واقع یه مسئلهای که هست گاهی ما یه سری موارد کد را نمی نویسیم ولی تو فایل اجرایی cpu هست. اون این چیز هایی هست که یه خورده باید بیشتر در موردشون صحبت کنیم.
@geek_ops
در آغاز بیایم یه مثال ساده را باهم بررسی کنیم، کد زیر را نگاه کنید، C++ با کامپایلر ARM gcc بدون optimization کامپایل کردم خروجی اسمبلی را هم میزارم.
اسمبلی:
تو این کد طبق معادل اسمبلی که گذاشتم، همینطور که میبینید destructor بعد فانکشن getNum کال میشه ولی هیچی انجام نمیشه عملا فقط instructions یه فعاليت بیهوده انجام میدهند.
اگر بیام و destructor را کامنت کنم یا معادل تعریف بصورت:
بکنم تغییرات کد اسمبلی را ببینید:
میبینید عملا دیگه destructor توی نتیجه ی نهایی وجود نداره، همینطور حجم کد باینری کم تر میشه.
پس:
۱- اگر فانکشنی استفاده نمیشه از کد حذفش کن
۲ - اگر destructor قرار نیست کاری انجام بده بطور مثال deallocation یا هر چیز دیگری، نیاز نیست تعریفش بکنی.
توی این مثال هم حجم کد خروجی کاهش پیدا کرد، هم از جا به جایی بیهوده instruction یا به نوعی runtime overhead جلوگیری شد.
نقل قول پایانی از کتاب:
@geeks_ops
class MyClass
{
private:
int num;
public:
MyClass(int t_num) : num(t_num) {}
~MyClass() {}
int getNum() const
{
return num;
}
};
int main()
{
MyClass obj(1);
return obj.getNum();
}
اسمبلی:
MyClass::MyClass(int) [base object constructor]:
push {r7}
sub sp, sp, #12
add r7, sp, #0
str r0, [r7, #4]
str r1, [r7]
ldr r3, [r7, #4]
ldr r2, [r7]
str r2, [r3]
ldr r3, [r7, #4]
mov r0, r3
adds r7, r7, #12
mov sp, r7
ldr r7, [sp], #4
bx lr
MyClass::~MyClass() [base object destructor]:
push {r7}
sub sp, sp, #12
add r7, sp, #0
str r0, [r7, #4]
ldr r3, [r7, #4]
mov r0, r3
adds r7, r7, #12
mov sp, r7
ldr r7, [sp], #4
bx lr
MyClass::getNum() const:
push {r7}
sub sp, sp, #12
add r7, sp, #0
str r0, [r7, #4]
ldr r3, [r7, #4]
ldr r3, [r3]
mov r0, r3
adds r7, r7, #12
mov sp, r7
ldr r7, [sp], #4
bx lr
main:
push {r4, r7, lr}
sub sp, sp, #12
add r7, sp, #0
adds r3, r7, #4
movs r1, #1
mov r0, r3
bl MyClass::MyClass(int) [complete object constructor]
adds r3, r7, #4
mov r0, r3
bl MyClass::getNum() const
mov r4, r0
nop
adds r3, r7, #4
mov r0, r3
bl MyClass::~MyClass() [complete object destructor]
mov r3, r4
mov r0, r3
adds r7, r7, #12
mov sp, r7
pop {r4, r7, pc}
تو این کد طبق معادل اسمبلی که گذاشتم، همینطور که میبینید destructor بعد فانکشن getNum کال میشه ولی هیچی انجام نمیشه عملا فقط instructions یه فعاليت بیهوده انجام میدهند.
اگر بیام و destructor را کامنت کنم یا معادل تعریف بصورت:
~MyClass() {} = default;بکنم تغییرات کد اسمبلی را ببینید:
MyClass::MyClass(int) [base object constructor]:
push {r7}
sub sp, sp, #12
add r7, sp, #0
str r0, [r7, #4]
str r1, [r7]
ldr r3, [r7, #4]
ldr r2, [r7]
str r2, [r3]
ldr r3, [r7, #4]
mov r0, r3
adds r7, r7, #12
mov sp, r7
ldr r7, [sp], #4
bx lr
MyClass::getNum() const:
push {r7}
sub sp, sp, #12
add r7, sp, #0
str r0, [r7, #4]
ldr r3, [r7, #4]
ldr r3, [r3]
mov r0, r3
adds r7, r7, #12
mov sp, r7
ldr r7, [sp], #4
bx lr
main:
push {r7, lr}
sub sp, sp, #8
add r7, sp, #0
adds r3, r7, #4
movs r1, #1
mov r0, r3
bl MyClass::MyClass(int) [complete object constructor]
adds r3, r7, #4
mov r0, r3
bl MyClass::getNum() const
mov r3, r0
nop
mov r0, r3
adds r7, r7, #8
mov sp, r7
pop {r7, pc}
میبینید عملا دیگه destructor توی نتیجه ی نهایی وجود نداره، همینطور حجم کد باینری کم تر میشه.
پس:
۱- اگر فانکشنی استفاده نمیشه از کد حذفش کن
۲ - اگر destructor قرار نیست کاری انجام بده بطور مثال deallocation یا هر چیز دیگری، نیاز نیست تعریفش بکنی.
توی این مثال هم حجم کد خروجی کاهش پیدا کرد، هم از جا به جایی بیهوده instruction یا به نوعی runtime overhead جلوگیری شد.
نقل قول پایانی از کتاب:
You don't pay for what you don't use
@geeks_ops
👍3
توی کلاس اگر ما private member داشته باشیم نیاز میشه با constructor مقدار دهی بشه، حالا اگه private member نداشته باشیم اون موقع نیازی به constructor نیست در نتیجه نیازی به فانکشن های getter و setter هم نیست، حالا میتوانیم کلاس را با structure جایگزین کنیم، چرا؟
چون default access یه structure از نوع public هست.
کد زیر را ببینیم:
اسمبلی:
حجم کد باینری چقدر پایین اومد، کافیه دقیق بدانیم قرار به چه نتیجه برسیم اونوقت میشه یه کد بهینه نوشت.
@geek_ops
چون default access یه structure از نوع public هست.
کد زیر را ببینیم:
struct MyClass
{
int num;
};
int main()
{
MyClass obj {1};
return obj.num;
}
اسمبلی:
main:
push {r7}
sub sp, sp, #12
add r7, sp, #0
movs r3, #1
str r3, [r7, #4]
ldr r3, [r7, #4]
mov r0, r3
adds r7, r7, #12
mov sp, r7
ldr r7, [sp], #4
bx lr
حجم کد باینری چقدر پایین اومد، کافیه دقیق بدانیم قرار به چه نتیجه برسیم اونوقت میشه یه کد بهینه نوشت.
@geek_ops
تاثیرات فعال کردن بهینه سازی کامپایلر تو پوشاندن یه خطا را بخوایم ببینیم میتوانیم با مثال زیر بفهمیم، اما یه مسئله ای را باید توجه کنیم زمانی که برای ساختار با حساسیت بالا برنامه نوشته میشه موقع کامپایل از هیچ گونه بهینه سازی انجام نمیشه، چرا؟؟ چون هیچ علاقه ای نداریم قسمت هایی از کد حذف یا تغییر کن و اساسأ باید کد مستقیم به زبان ماشین تبدیل بشه.
اگه بیایم به ماکزیمم مقدار یه متغیر یه واحد اضافه کنیم چه اتفاقی میفته؟
چیزی که بنده مطالعه کردم و تو کتاب هم بهش ذکر شده اینه که کامل وابسته به نوع کامپایل و سخت افزار داره اینجا اومدم کد را با g++ بدون بهینه سازی کامپایل کردم.
میبینید مشکل جدی شد... فرض کنید روی یک سیستم حیاتی بدون تعبيه خطایی کد روند خودش را با مقدار اشتباه ادامه بده.
منجر به خطا تو کل سیستم میشه، اگر بیام بهینه سازی کامپایلر را برای کامپایلر ARM فعال کنم چه نتیجه ای میگریم؟ بزارید یک اسمبلی را بررسی کنیم.
همانطور که میبینید مقدار ماکزیمم int وارد تابع میشه و integer overflow رخ میدهد.
اما با کامپایل کردن به بهینه سازی تابع foo همیشه مقدار ۱ را بر میگردونه و هیچ عملیاتی انجام نمیده.
@geek_ops
bool foo(int x)
{
int y = x + 1;
return y > x;
}
int main()
{
if (foo(std::numeric_limits<int>::max()))
{
printf("X larger than X+1\r\n");
}
else
{
printf("X is NOT larger than X + 1. Oh nooo!\r\n");
}
return 0;
}
اگه بیایم به ماکزیمم مقدار یه متغیر یه واحد اضافه کنیم چه اتفاقی میفته؟
چیزی که بنده مطالعه کردم و تو کتاب هم بهش ذکر شده اینه که کامل وابسته به نوع کامپایل و سخت افزار داره اینجا اومدم کد را با g++ بدون بهینه سازی کامپایل کردم.
X larger than X+1
میبینید مشکل جدی شد... فرض کنید روی یک سیستم حیاتی بدون تعبيه خطایی کد روند خودش را با مقدار اشتباه ادامه بده.
منجر به خطا تو کل سیستم میشه، اگر بیام بهینه سازی کامپایلر را برای کامپایلر ARM فعال کنم چه نتیجه ای میگریم؟ بزارید یک اسمبلی را بررسی کنیم.
foo(int):
push {r7}
sub sp, sp, #20
add r7, sp, #0
str r0, [r7, #4]
ldr r3, [r7, #4]
adds r3, r3, #1
str r3, [r7, #12]
ldr r2, [r7, #12]
ldr r3, [r7, #4]
cmp r2, r3
ite gt
movgt r3, #1
movle r3, #0
uxtb r3, r3
mov r0, r3
adds r7, r7, #20
mov sp, r7
ldr r7, [sp], #4
bx lr
.LC0:
.ascii "X larger than X+1\015\000"
.LC1:
.ascii "X is NOT larger than X + 1. Oh nooo!\015\000"
main:
push {r7, lr}
add r7, sp, #0
bl std::numeric_limits<int>::max()
mov r3, r0
mov r0, r3
bl foo(int)
mov r3, r0
cmp r3, #0
beq .L6
movw r0, #:lower16:.LC0
movt r0, #:upper16:.LC0
bl puts
b .L7
.L6:
movw r0, #:lower16:.LC1
movt r0, #:upper16:.LC1
bl puts
.L7:
movs r3, #0
mov r0, r3
pop {r7, pc}
همانطور که میبینید مقدار ماکزیمم int وارد تابع میشه و integer overflow رخ میدهد.
foo(int):
movs r0, #1
bx lr
.LC0:
.ascii "X larger than X+1\015\000"
main:
push {r3, lr}
movw r0, #:lower16:.LC0
movt r0, #:upper16:.LC0
bl puts
movs r0, #0
pop {r3, pc}
اما با کامپایل کردن به بهینه سازی تابع foo همیشه مقدار ۱ را بر میگردونه و هیچ عملیاتی انجام نمیده.
@geek_ops
two_data_type.s
1.4 KB
تو پست های قبلی در مورد ring buffer با data type های مختلف که صحبت میکردیم به این اشاره کردیم که میشه از templates ها توی c++ استفاده کرد و data type های مختلف را استفاده کرد.
اما مسئله ای که بعدا بهش اشاره کردیم runtime overhead and Code Bloat بود اینکه تاثیر templates تو این دو تا قضیه چیه؟
گفتیم که templates اساسأ سایز کد خروجی یا همون فایل باینری را افزایش میده اما چرا؟
اگر یه صحبتی بکنیم در موردش میشه از اینجا شروع کرد.
الان کد بالا را ببنید یه template class که دو تا instance با int و double گرفته شده الان در واقع دو تا symbol table از MyClass ساخته میشه برای دو تا data type مختلف در نتیجه سایز کد باینری افزایش پیدا میکنه برای درک بهتر فایل های اسمبلی را ضمیمه میکنم
@geek_ops
اما مسئله ای که بعدا بهش اشاره کردیم runtime overhead and Code Bloat بود اینکه تاثیر templates تو این دو تا قضیه چیه؟
گفتیم که templates اساسأ سایز کد خروجی یا همون فایل باینری را افزایش میده اما چرا؟
اگر یه صحبتی بکنیم در موردش میشه از اینجا شروع کرد.
template<typename T>
class MyClass
{
private:
T set_;
public:
MyClass(T set) : set_(set) {}
T getT()
{
return set_;
}
};
int main()
{
MyClass<int> test1(56);
MyClass<double> test2(2.36);
return 0;
}
الان کد بالا را ببنید یه template class که دو تا instance با int و double گرفته شده الان در واقع دو تا symbol table از MyClass ساخته میشه برای دو تا data type مختلف در نتیجه سایز کد باینری افزایش پیدا میکنه برای درک بهتر فایل های اسمبلی را ضمیمه میکنم
@geek_ops
درود خدمت دوستان، ادامه ی مبحث code bloat دو تا مبحث مهم میمونه که باعث:
1 - RTTI
2 - Exceptions
قبل از اینکه به این دو مورد بپردازیم نیازه که یه سری تعاریف و مثال های زده باشه تا خوب بتوانیم متوجه بشیم دلیل افزایش حجم کد خروجی چیه؟
چیزایی که قراره در موردش صحبت کنم در ادامه را اینجا مینویسم که یادم نره:
1 - Dynamic Dispatching
2 - V-Table
3 - Stack Unwinding
#RTTI
#dynamic
@geek_ops
1 - RTTI
2 - Exceptions
قبل از اینکه به این دو مورد بپردازیم نیازه که یه سری تعاریف و مثال های زده باشه تا خوب بتوانیم متوجه بشیم دلیل افزایش حجم کد خروجی چیه؟
چیزایی که قراره در موردش صحبت کنم در ادامه را اینجا مینویسم که یادم نره:
1 - Dynamic Dispatching
2 - V-Table
3 - Stack Unwinding
#RTTI
#dynamic
@geek_ops
1 - Dynamic Dispatching
توضیحش را بزارید اینطور شروع کنم فرض کنید شما چند وسیله ی برقی دارید و یه کنترل اگر دکمه روشن این کنترل بزنم، یه مکانیزمی وجود داشته باشه که باید تشخيص بده کدام وسیله داخل پریز برق هست همان را روشن کنه.
بهتر بخوام بگم فرض کنید با زدن دکمه روشن اگر تلویزیون داخل پریز هست اون روشن کنه اگه یه وسیله ی دیگه اون را روشن کنه و الی آخر.
همین قضیه را بخوام تعمیم بدهم به موضوع خودمون اینطور میشه گفت که اگر یه سری class methods داشته باشیم و در برنامه در جاهای مختلف call شده باشند اصطلاحا بهش میگیم static dispatching حین کامپایل مکان ها مشخص و روی instructions ها به اجرا میشوند.
حالا اگر یه class با حتی یک virtual method وجود داشته باشه دیگه عملا مکانیزم قبلی قابل اجرا نیست.
هر کلاس با حداقل یک virtual method حین کامپایل یه V-table براش ساخته میشه که مجموعه ای از object pointers هستند که موقع runtime برنامه تشخيص داده میشه که method از کدام object pointer صدا زده شده.
مورد اول در مورد v-table باید بیشتر صحبت کنیم و مورد دوم اینکه همین عملکرد هم باعث code bloat و هم باعث runtime overhead میشه.
بزارید برای درک بیشتر یه مثال کوچک بزنیم:
خروجی بالا را ببینیم با g++ بدون flag کامپایل شده:
نکته ی مهم object از Car ساخته شد ولی destroyed نشد destructor صدا زده نشد.
حالا درست شد، وقتی شما virtual method داخل class دارید و derive class تعریف میکنید باید destructor base class، virtual تعریف بشه.
کد نهایی را برای arm کامپایل کردم خروجی و نحوه ی تخصیص v-table را ببینیم.
در مورد v-table و نحوه ی تخصیص بیشتر صحبت میکنم فعلا برم ببینم نحوه ی شبیه سازیش را تو C چجوری میتوانم پیاده کنم.
#Cplusplus
@geek_ops
توضیحش را بزارید اینطور شروع کنم فرض کنید شما چند وسیله ی برقی دارید و یه کنترل اگر دکمه روشن این کنترل بزنم، یه مکانیزمی وجود داشته باشه که باید تشخيص بده کدام وسیله داخل پریز برق هست همان را روشن کنه.
بهتر بخوام بگم فرض کنید با زدن دکمه روشن اگر تلویزیون داخل پریز هست اون روشن کنه اگه یه وسیله ی دیگه اون را روشن کنه و الی آخر.
همین قضیه را بخوام تعمیم بدهم به موضوع خودمون اینطور میشه گفت که اگر یه سری class methods داشته باشیم و در برنامه در جاهای مختلف call شده باشند اصطلاحا بهش میگیم static dispatching حین کامپایل مکان ها مشخص و روی instructions ها به اجرا میشوند.
حالا اگر یه class با حتی یک virtual method وجود داشته باشه دیگه عملا مکانیزم قبلی قابل اجرا نیست.
هر کلاس با حداقل یک virtual method حین کامپایل یه V-table براش ساخته میشه که مجموعه ای از object pointers هستند که موقع runtime برنامه تشخيص داده میشه که method از کدام object pointer صدا زده شده.
مورد اول در مورد v-table باید بیشتر صحبت کنیم و مورد دوم اینکه همین عملکرد هم باعث code bloat و هم باعث runtime overhead میشه.
بزارید برای درک بیشتر یه مثال کوچک بزنیم:
class Car
{
public:
Car() {std::cout << "Constructor\n";}
virtual void printCars() = 0;
~Car() {std::cout << "Destructor\n";}
};
class Benz : public Car
{
public:
Benz() {std::cout << "Constructor Benz\n";}
void printCars()
{
std::cout << "Hello Benz\n";
}
~Benz() {std::cout << "Destructor Benz\n";}
};
int main()
{
Car* c = new Benz();
c->printCars();
return 0;
}
خروجی بالا را ببینیم با g++ بدون flag کامپایل شده:
Constructor
Constructor Benz
Hello Benz
نکته ی مهم object از Car ساخته شد ولی destroyed نشد destructor صدا زده نشد.
class Car
{
public:
Car() {std::cout << "Constructor\n";}
virtual void printCars() = 0;
virtual ~Car() {std::cout << "Destructor\n";}
};
class Benz : public Car
{
public:
Benz() {std::cout << "Constructor Benz\n";}
void printCars()
{
std::cout << "Hello Benz\n";
}
~Benz() {std::cout << "Destructor Benz\n";}
};
int main()
{
Car* c = new Benz();
c->printCars();
delete c;
return 0;
}
حالا درست شد، وقتی شما virtual method داخل class دارید و derive class تعریف میکنید باید destructor base class، virtual تعریف بشه.
کد نهایی را برای arm کامپایل کردم خروجی و نحوه ی تخصیص v-table را ببینیم.
vtable for Benz:
.word 0
.word typeinfo for Benz
.word Benz::printCars()
vtable for Car:
.word 0
.word typeinfo for Car
.word __cxa_pure_virtual
typeinfo for Benz:
.word _ZTVN10__cxxabiv120__si_class_type_infoE+8
.word typeinfo name for Benz
.word typeinfo for Car
typeinfo name for Benz:
.ascii "4Benz\000"
typeinfo for Car:
.word _ZTVN10__cxxabiv117__class_type_infoE+8
.word typeinfo name for Car
typeinfo name for Car:
.ascii "3Car\000"
در مورد v-table و نحوه ی تخصیص بیشتر صحبت میکنم فعلا برم ببینم نحوه ی شبیه سازیش را تو C چجوری میتوانم پیاده کنم.
#Cplusplus
@geek_ops
👍1
2 - V-Table
وقتی در مورد dispatching صحبت کردیم به این اشاره کردیم باید مکانیزمی وجود داشته باشه که بشه باهاش تشخيص داد چه فانکشنی از چه instance صدا زده شده، وقتی که برای کلاس یک virtual method تعریف میکنیم کامپایلر برای این کلاس و تمام inherent های همین کلاس، جور دیگه رفتار میکنه چرا؟
اینجاست که توی کامپایلر یه پوینتر به هر کلاس اختصاص میشه و این پوینتر اشاره داره به vtable خود کلاس، یه نمای کلی رو توی تصویری که ضمیمه همین پست هست ارسال کردم.
عملا با توضیحاتی که دادم میشه این رو متوجه شد توی حین اجرای کد اگر فانکشن virtual صدا زده بشه میزان جابه جایی instructions و stack pointer بیشتر از حالتی هست که یه فانکشن غیر virtual صدا زده میشه، برای همینه که تعریف فانکشن یا کلی تر polymorphism هم باعث code bloat و هم باعث runtime overhead میشه.
یه کدی با C نوشتم که بشه باهاش V-Table را بهتر فهمید پایین همین پست میزارم کامپایل کنید.
سعی کنید موقع صدا زدن فانکشن ها آدرسشون و نحوه ی عمل کردن instructions را هم ببینید، اسمبلی اش را نمیزارم ولی حتما خودتون یه نگاهی بهش بندازید.
#V_Table
#C
@geek_ops
وقتی در مورد dispatching صحبت کردیم به این اشاره کردیم باید مکانیزمی وجود داشته باشه که بشه باهاش تشخيص داد چه فانکشنی از چه instance صدا زده شده، وقتی که برای کلاس یک virtual method تعریف میکنیم کامپایلر برای این کلاس و تمام inherent های همین کلاس، جور دیگه رفتار میکنه چرا؟
اینجاست که توی کامپایلر یه پوینتر به هر کلاس اختصاص میشه و این پوینتر اشاره داره به vtable خود کلاس، یه نمای کلی رو توی تصویری که ضمیمه همین پست هست ارسال کردم.
عملا با توضیحاتی که دادم میشه این رو متوجه شد توی حین اجرای کد اگر فانکشن virtual صدا زده بشه میزان جابه جایی instructions و stack pointer بیشتر از حالتی هست که یه فانکشن غیر virtual صدا زده میشه، برای همینه که تعریف فانکشن یا کلی تر polymorphism هم باعث code bloat و هم باعث runtime overhead میشه.
یه کدی با C نوشتم که بشه باهاش V-Table را بهتر فهمید پایین همین پست میزارم کامپایل کنید.
سعی کنید موقع صدا زدن فانکشن ها آدرسشون و نحوه ی عمل کردن instructions را هم ببینید، اسمبلی اش را نمیزارم ولی حتما خودتون یه نگاهی بهش بندازید.
#V_Table
#C
@geek_ops
👍1
V-Table in C
#CleanCode
#Code_Bloat
#Runtime
@geek_ops
#include <stdio.h>
typedef void (*func)(void);
typedef void (*function1)(void);
typedef void (*function2)(void);
typedef struct
{
func vtable;
function1 func1;// Virtual Function (Base Class)
}Base;
typedef struct
{
func vtable;
function1 func1; // Virtual Function (override)
}D1;
typedef struct
{
func vtable;
function1 func1; // Virtual Function (override)
}D2;
void print_d1()
{
printf("Hello my D1\n");
}
void print_d2()
{
printf("Hello my D2\n");
}
int main()
{
/******************/
/ Compile Time /
/******************/
Base b;
b.vtable = b.func1;
D1 d1;
d1.vtable = b.func1;
d1.func1 = print_d1;
D2 d2;
d2.vtable = b.func1;
d2.func1 = print_d2;
/******************/
/****************************/
/ Polymorphism (V-Table) /
/****************************/
Base* f;
f = &d1;
f->func1();
f = &d2;
f->func1();
/****************************/
return 0;
}
#CleanCode
#Code_Bloat
#Runtime
@geek_ops
👍2
3 - Stack Unwinding
بخوام یه معنی مناسب براش پیدا کنم، میشه گفت بازگشایی پشته یا مثلا بازگردانی پشته یا هر چی.
یه کد وقتی اجرا میشه بستگی به کامپایلر و سخت افزار که هر مدل یه frame pointer مخصوص به خودش را داره.
پردازنده میاد frame pointer که حالا مثلا تو ساختار x86_64 اسمش rbp را فرامیخوانه این پوینتر یه اشاره گر که خیلی ساده بخوام بگم نشان دهنده ی اینه برنامه تو چه خطی از کد اسمبلی داره اجرا میشه، بزارید یه نمونه با هم ببینیم.
اگر فانکشن foo را نگاه کنید میبینید یه سری رفتار اضافه داره انجام میده، به چه صورت میاد یکسری allocate هایی روی heap انجام میده و از یکسری Metadata استفاده میکنه تا بتواند stack را به اصطلاح unwind کنه.
یه مقدار حافظه برای پیام throw و همینطور خود object Exceptions میاد قدم به قدم یکسری پوینتر، object و پیام throw را بر میگردونه تا به catch برسه.
کل این فرآیند باعث بوجود آمدن مشکلاتی که گفتم میشه.
نکته اگر خواستید کامپایلر را force کنید که به هیچ عنوان از این مکانیزم استفاده نکنه از flag های زیر استفاده کنید.
#RTTI
#C
#Exception
@geek_ops
بخوام یه معنی مناسب براش پیدا کنم، میشه گفت بازگشایی پشته یا مثلا بازگردانی پشته یا هر چی.
یه کد وقتی اجرا میشه بستگی به کامپایلر و سخت افزار که هر مدل یه frame pointer مخصوص به خودش را داره.
پردازنده میاد frame pointer که حالا مثلا تو ساختار x86_64 اسمش rbp را فرامیخوانه این پوینتر یه اشاره گر که خیلی ساده بخوام بگم نشان دهنده ی اینه برنامه تو چه خطی از کد اسمبلی داره اجرا میشه، بزارید یه نمونه با هم ببینیم.
.LC0:
.ascii "Cannot open\000"
foo():
push {r7, lr}
add r7, sp, #0
movs r0, #4 /* allocate r0 4 bytes */
bl __cxa_allocate_exception /*allocate memory for exceptions */
mov r3, r0 /* now r0 point to exception object */
mov r0, r3
movw r3, #:lower16:.LC0
movt r3, #:upper16:.LC0
str r3, [r0]
movs r2, #0
movw r1, #:lower16:typeinfo for char const*
movt r1, #:upper16:typeinfo for char const*
bl __cxa_throw
boo():
push {r7, lr} /*push stack pointer and frame pointer then*/
add r7, sp, #0 /*sp = r7 link to function foo()*/
bl foo() /**/
nop
pop {r7, pc}
main:
push {r7, lr} /*push stack pointer and frame then*/
sub sp, sp, #8 /*subtract 8 from sp and r7 = sp jump to boo() */
add r7, sp, #0 /**/
bl boo()
r7 frame pointer
sp stack pointer
اگر فانکشن foo را نگاه کنید میبینید یه سری رفتار اضافه داره انجام میده، به چه صورت میاد یکسری allocate هایی روی heap انجام میده و از یکسری Metadata استفاده میکنه تا بتواند stack را به اصطلاح unwind کنه.
یه مقدار حافظه برای پیام throw و همینطور خود object Exceptions میاد قدم به قدم یکسری پوینتر، object و پیام throw را بر میگردونه تا به catch برسه.
کل این فرآیند باعث بوجود آمدن مشکلاتی که گفتم میشه.
نکته اگر خواستید کامپایلر را force کنید که به هیچ عنوان از این مکانیزم استفاده نکنه از flag های زیر استفاده کنید.
-fno-exceptions
-fno-rtti
#RTTI
#C
#Exception
@geek_ops
👍2
#out_of_context
یه گزارش هک برای دیتابیس Mongo گزارش شده خیلی جالب بود وقتی داشتم بررسی میکردم.
وقتی میایم یه پوینتر به یه بلوک از حافظه تعریف میکنیم وقتی اون بلوک free میشه برنامه نمیاد اطلاعات موجود تو یه اون بلوک را صفر کنه میاد اینطور در نظر میگیره که خوب این بلوک آزاده حالا میتوانم خودم ازش دوباره استفاده کنم و اگر جای دیگه از کد memory خواست از این هم میتوانم استفاده کنم و دیتا عملا overwrite میشه.
از همین خلا استفاده کرده و توانسته اطلاعات را بیت به بیت استخراج کنه به این روش، علاقه مند بودید دو تا لینک این پایین میزارم کامل توضیح داده.
https://nvd.nist.gov/vuln/detail/CVE-2025-14847
https://bigdata.2minutestreaming.com/p/mongobleed-explained-simply
#hack
#Mongo
#c
@geek_ops
یه گزارش هک برای دیتابیس Mongo گزارش شده خیلی جالب بود وقتی داشتم بررسی میکردم.
وقتی میایم یه پوینتر به یه بلوک از حافظه تعریف میکنیم وقتی اون بلوک free میشه برنامه نمیاد اطلاعات موجود تو یه اون بلوک را صفر کنه میاد اینطور در نظر میگیره که خوب این بلوک آزاده حالا میتوانم خودم ازش دوباره استفاده کنم و اگر جای دیگه از کد memory خواست از این هم میتوانم استفاده کنم و دیتا عملا overwrite میشه.
از همین خلا استفاده کرده و توانسته اطلاعات را بیت به بیت استخراج کنه به این روش، علاقه مند بودید دو تا لینک این پایین میزارم کامل توضیح داده.
https://nvd.nist.gov/vuln/detail/CVE-2025-14847
https://bigdata.2minutestreaming.com/p/mongobleed-explained-simply
#hack
#Mongo
#c
@geek_ops
👍1