السلام عليكم ورحمة الله وبركاته
ليكن لدينا الكود التالي
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Hello world!\n");
printf("%f",1.5);
return 0;
}كيف يتم تخزين الأعداد العشرية والتعامل معها من قبل المعالج ؟
إليك ما يلي
لطباعة العدد 1.5
قام البرنامج بدفع قيمتين للدالة كوسيط كما يلي
الأولى 0 ( سنتحدث عنها في آخر المقالة )
والثانية 3FF80000
فماذا تعني كل واحدة ؟
نحن طلبنا العدد 1.5 فأين هو ؟
القيمة الثانية 3ff80000 هي
0011 1111 1111 1000 0000 0000 0000 0000
فأين ماذا من كيف ؟ ×_×
لنبدأ .... (يمكنك متابعة الشرح بنفسك إن كان عندك برنامج تنقيح مثل ollydbg ابحث عن HelloWorld وتابع البرنامج من هناك .. فالرحلة ممتعة)
لنجرب تغيير أول واحد من اليمين .. يعني بدل 8 سنضع صفر
!!!
قام البرنامج بطباعة 1 بدل 1.5
لنجرب الآن أن نغير الواحد الثاني
بدل F وهي 1111سنضع1110 أي وهو E
ثم سنجرب C ثم سنجرب 8
من أجل E طبع 0.5
ومن أجل C طبع 0.125
ومن أجل 8 أعطى 0.007813
إذا سنتوقف عند C ... ونتابع استنتاجها لاحقا
لو وضعنا 80000000 سيعطينا -0 ويبدو أن آخر بت هو بت الإشارة .. هذا جيد
للتأكد نضع BFF80000 بدلا من 3FF80000 أي نغير فقط آخر بت والنتيجة -1.5
هذا جيد
لو وضعنا 00C00000 فسيعطي صفرا :(
لكن لا بد أن هناك أرقام أخرى بعد الفاصلة .. لذا سنغير %f في الكود إلى %.30f
بمجرد إزالة 3 ووضع صفر فالعدد الناتج يكون صفراً ..
لنحاول ألا نقترب من البتات اليسارية كثيراً وفق ما يلي
3 F F 8 0 0 0 0 0011 1111 1111 1000 0000 0000 0000 0000
كل البتات التي نتكلم عنها سنعدها من اليسار لليمين
الأول للإشارة
من الثاني إلى التاسع تسبب مشاكل :)
من العاشر إلى الثالث عشر سنختبرها الآن
لنجرب الآن
3F780000=0011 1111 0111 1000 0000 0000 0000 0000 0.005859375000
لنجرب 3FB80000=0011 1111 1011 1000 0000 0000 0000 0000 0.093750000000 لنجرب 3FD80000=0011 1111 1101 1000 0000 0000 0000 0000 0.375000000000 لنجرب 3FE80000=0011 1111 1110 1000 0000 0000 0000 0000 0.750000000000 وطبعا جربنا 3FF00000=0011 1111 1111 0000 0000 0000 0000 0000 1.000000000000
ما المعنى من كل هذا ؟؟
لنتابع الآن بزيادة واحدات بدلا من حذفها
3FFC0000=0011 1111 1111 1100 0000 0000 0000 0000 1.75 3FFE0000=0011 1111 1111 1110 0000 0000 0000 0000 1.875000
هل يمكننا اعتبار البت الخامس عشر يساوي 0.125
لنجرب ما يلي
لدينا
3FF00000=0011 1111 1111 0000 0000 0000 0000 0000 1.000000000000 هل هذا يعني أن 3FF20000=0011 1111 1111 0010 0000 0000 0000 0000 تساوي 1.125
!! نعم
ماذا عن
3FF10000=0011 1111 1111 0001 0000 0000 0000 0000
هل تساوي 1.0625
(0.0625=0.125 مقسومة على 2)
!! نعم !!
إلى أي حد يا ترى ؟
3FF00001=0011 1111 1111 0000 0000 0000 0000 0001
المفترض أن يساوي 1مقسوم على 2^20
أي 0.00000095367431640625
الناتج
1.000000953674316400000000000000
طبعاً المشكلة في في دالة العرض على أي حال ..
والآن لنعد إلى البتات الأوائل من 2 إلى 12
نعلم أن
3FF00000=0011 1111 1111 0000 0000 0000 0000 0000
يعطي قيمة الواحد
ماذا عن
3FE00000=0011 1111 1110 0000 0000 0000 0000 0000
يعطي 0.5 !
هل هذا يعني أن البت 12 وزنه 0.5
لنتابع
3FC00000=0011 1111 1100 0000 0000 0000 0000 0000 = 0.125 3F800000=0011 1111 1000 0000 0000 0000 0000 0000 = 0.0078125
بدأت الأمور تخرج عن السيطرة ! فالنتيجة السابقة هي 1 مقسوم على 2^7
قبل المتابعة علينا التأكد من آخر استنتاج
3FC00000=0011 1111 1100 0000 0000 0000 0000 0000 = 0.125 3FF00000=0011 1111 1111 0000 0000 0000 0000 0000 = 1.000 ---------------------------------------------------------- 3FD00000=0011 1111 1101 0000 0000 0000 0000 0000 = 0.625 ؟؟
خطأ !! = 0.25
أي كأننا ضربنا 0.125 بـ 2!!
لاحظ أن
3FF00000=0011 1111 1111 0000 0000 0000 0000 0000 = 1.000 بينما 3FE00000=0011 1111 1110 0000 0000 0000 0000 0000 = 0.500
أي كأننا قسمنا على 2
فهل تعني البتّات ال12-11-10-9 الضرب بقوى العدد 2!
عندما يكون البت واحداً نضرب بـ2 !!
ولكن انظر ::
3FF
00000=0011 1111 1111 0000 0000 0000 0000 0000 = 1.000 /2 =0.5 3FE00000=0011 1111 1110 0000 0000 0000 0000 0000 = 0.500 /2 =0.25 /2 =0.125 3FC00000=0011 1111 1100 0000 0000 0000 0000 0000 = 0.125 /2 =0.0625 /2 =0.3125 /2= 0.15625 /2 =0.078125 3F800000=0011 1111 1000 0000 0000 0000 0000 0000 = 0.078125
وضحت الفكرة !!
البت 12 يكافئ الضرب بـ2 وهي 2 مرفوعة لـ1
بينما يكافئ البت 11 الضرب بـ 4 وهي 2 مرفوعة لـ2
هكذا .. البت 10 للضرب بـ 16 وهي 2 مرفوعة لـ4
والبت 9 للضرب بـ 256 وهي 2 مرفوعة لـ8
فهل هذا يعني أن
3F000000=0011 1111 0000 0000 0000 0000 0000 0000 = 0.00003051757812500
أي نقسم آخر جواب على 256
النتيجة
0.00003051757812500
بالفعل !!صحيح :)
لنجرب إلى أي مدى استنتاجنا صحيح ؟ ولكن يجب أن نحذر من الانحدار الكبير للقيم .. والأخلاق
3E000000=0011 1110 0000 0000 0000 0000 0000 0000 = 0.00003051757812500/65536=0.0000000004656612873077392578125 ==>0.000000000465661287307739260000 مقبول 3C000000=0011 1100 0000 0000 0000 0000 0000 0000 = 0.0000000004656612873077392578125/4294967296=1.0842021724855044340074528008699e-19
اظن أن هذه الدقة كبيرة بما يكفي !!
0.000000000000000000108420217249 ممتاز
لنتابع
38000000=0011 1000 0000 0000 0000 0000 0000 0000 = 1.0842021724855044340074528008699e-
19/18446744073709551616=5.877471754111437539843682686111e-39
يا إلهي !! الآن عرفت كيف يمكن لشيء كهذا أن يتحمل قيماً كبيرة إلى هذ الحد !!
فهو أصلاً يحتوي هذه القيم الكبيرة بشكل أو بآخر ..
مثلاً لو قلت لك .. إذا رفعت إبهامي فهذا يعني مليون وإذا رفعت السبابة فهي تعني مليار مليار
فيمكنني باستخدام اصبعين فقط أن أرمّز 4 أرقام كبيرة جداً .. (طبعاً مع دقّة سيئة جدّاً)
لنتابع :)
ولكن علينا تغيير الكود من %.30fإلى %g
30000000=0011 0000 0000 0000 0000 0000 0000 0000 =1.72723371101888892507727037256e-77
ويظهر على الشاشة
1.72723e-077 وهذا شيء مذهل :)
10000000=0010 0000 0000 0000 0000 0000 0000 0000 =1.4916681462400413486581930630925e-154
1.28823e-231
ماذا بقي !!
00000000=0000 0000 0000 0000 0000 0000 0000 0000 =1.1125369292536006915451163586661e-308
يطبعها صفراً
ولكن ماذا عن البت الثاني ؟
40000000=0100 0000 0000 0000 0000 0000 0000 0000
غريب ! النتيجة 2 :D
طيب هل يمكنك استنتاج قيمة
60000000=0110 0000 0000 0000 0000 0000 0000 0000
تذكر 2 مضروبة بــ 2^512 :)
يعني
2^513 ~=2.68156e+154
ممتاز !!
إذا يمكننا اعتبار ما يلي:
إذا كانت كل البتات أصفار فإن :
العدد يساوي
1.1125369292536006915451163586661e-308
وهو تماماً 2 مرفوعة للقوة السالبة 1023
2^(-1023)
أما إن كان البت الثاني من اليسار واحداً
فإن العدد يساوي
2+2^(-1023)
ونعتبره تقريباً
2
أنا سعيد جدّاً في هذه اللحظة .. :)
لنتابع
لم يبق شيء :D
لنتوقّف إذا ونراجع استنتاجاتنا
يتم ترميز الأعداد داخل متغير من نوع float بالشكل التالي
SDPP PPPP PPPP NNNN NNNN NNNN NNNN NNNN
S الإشارة , صفر تعني موجب , وواحد تعني سالب
D إذا كانت واحد نضيف 2 إلى العدد , وإن كانت صفر نعتبر أن العدد لدينا هو 2 مرفوعة للقوة 1023 سالبة
P تؤدي إلى ضرب العدد بـ2 مرفوعة إلى القوة (2 مرفوعة إلى القوة رقم البت)
أول بت P من اليمين يؤدي إلى ضرب العدد بـ 2 مرفوعة إلى القوة (2مرفوعة إلى القوة 0) أي 2
الثاني من اليمين يؤدي للضرب بـ4
الثالث 16
وهكذا .. 256..65536..4294967296..18446744073709551616..
3.4028236692093846346337460743177e+38
1.1579208923731619542357098500869e+77
والأول من اليسار
1.3407807929942597099574024998206e+154
N تؤدي إلى جمع قوى العدد2 السالبة إلى الرقم حسب مكان البت
الأول من اليسار القوة 1 السالبة أي 0.5
الثاني من اليسار القوة 2 السالبة أي 0.25
وهكذا
0.125 - 0.0625 - 0.03125 - 0.015625 - .... - 0.00000095367431640625
وهكذا نكون قد أنهينا فصفصة عظام ومفاصل ترميز الأعداد الكسرية
ونكتشف في النهاية أن الترميز جميل ومتقن , ولكن البرمجة ليست في الترميز ,
بل في الدالة printf التي تقوم بتحويل الترميز الثنائي هذا إلى قيمة مقروءة من قبل حضرتك
بل لنتكلّم بشكل أوسع ..
من أهم ميزات معالجات الـ32 بت أنها أتاحت لك وبشكل هاردويري أن تقوم بجمع عددين يستخدمان ترميز معقّد كهذا !!
تخيل كم توفّر عليك التعليمة FADD من عمليات !!
حاول التفكير في طريقة لجمع رقمين بهذا الترميز .. حاول كتابة برنامج يقوم بذلك ..
لتعرف حجم التطوير الكبير في معالج إحدى تعليماته FADD :)
float x=1.5;float y=1.5;float z=x+y;
لو كانت عملية الجمع السابقة تحدث برمجيّاً , فعليك نسيان لعبة Pro Evolution Soccer ونسيان Counter Strike و برامج التصميم مثل maya وأخواتها .. والاكتفاء بماريو على كونسول سوداء ملوّنة :)
أرجو أن تكون الرحلة ممتعة لك كما كانت ممتعة لي :)
بالمناسبة استخدمت gcc للترجمة وCode::blocks كبيئة وطبعاً للتنقيح وتغيير قيمة الوسيط المرر لـ printf استخدمت الصديق الصدوق olly dbg إن كان ذلك يهمّ أحداً ..
أخيراً يجب أن أذكر أمراً كنت قد تغاضيت عنه ..
وهو القيمة الأولى التي يتم تمريرها كجزء من العدد وكانت قيمتها صفر طوال مدة العرض
هذه القيمة ذات 32 بت جميعها تكملة لبتّات N الخاصة بحفظ قيمة الرقم وهي لزيادة الدقة
أما سبب التغاضي عنها فهو أنني من المفترض أنني أتعامل مع float وليس مع double !!
ولم أفهم سبب التعامل مع float دوماً كـ8بايت كاملة
حتى عبارة مثل هذه
printf("%f",1.5f);تتسبب في دفع 8بايت للمكدس ..
على أي حال هي لزيادة الدقة .. وهذا جيد دوماً :P ..
(تمت كتابة المقالة in real time لذلك سامحونا للأخطاء المطبعية )
بالتوفيق للجميع
والسلام عليكم ورحمة الله وبركاته