این اولین قسمت از فصل اول آموزش FreeRTOS در STM32 نوشتن یک سیستمعامل بلادرنگ برای میکروکنترلرهاست. در این قسمت به بررسی کامپایلر GCC می پردازیم. همانطور که قبلاً گفتم فصل اول درباره پیشنیازهای نوشتن یک سیستمعامل بلادرنگ است. تمامی کسانی میخواهند بهعنوان توسعهدهنده در حوزه میکروکنترلرها فعالیت کنند، باید با یک سری نرمافزارها و ابزارها و یک سری مفاهیم مرتبط با میکروکنترلرها آشنایی داشته باشند.
پس داشتن شناخت عمیق در هر حوزهای برای یک توسعهدهنده آن حوزه از نون شب واجب تره. در این قسمت میخوام دررابطهبا ابزاری صحبت کنم که اگه ساخته نمیشد ما باید با صفر و یک برنامه مینوشتیم. درست حدس زدید کامپایلرها . کامپایلر مفهومی است که امروزه به لطف توسعهدهندگان سیار گسترده و بزرگ شده؛ اما در این قسمت ما با کامپایلرها و تفاوتشان با IDEها آشنا میشیم و به سؤالاتی از قبیل اینکه اصلاً IDE چی هست؟ کامپایلر چیست؟ از کدوم کامپایلر استفاده کنیم بهتره؟ چطور یک کامپایلر نصب کنیم؟ حالا که نصب کردیم چطور ازش استفاده کنیم؟ و سؤالاتی ازاینقبیل پاسخ میدیم. این سؤالات اولین و ابتداییترین سؤالاتی هستند که با ورود به دنیای برنامهنویسی برامون ایجاد میشه که قراره باحوصله به اونها پاسخ بدیم. پس با سیسوگ همراه باشید.
کامپایلر چیست؟؟؟
کامپایلر در واقع یک ابزار نرمافزاری است که برنامه ما رو به فایلی تبدیل میکند که توسط کامپیوتر،Socها یا MCUها قابلاجرا باشد و از اونجا که ما با میکروکنترلر کار میکنیم، اغلب منظورمون از برنامه، کدی است که به زبان C یا ++C نوشته شده باشد.پس کامپایلر اون نرمافزاری نیس که بتونی کدت رو داخلش ادیت کنی. |
خوب کامپایلرهای متفاوتی برای تبدیل کد C یا ++C برای سیستمعامل وجود دارد.مانند:
- کامپایلر GCC که مخفف GNU Compiler Collection هست و توسط ریچارد استالمن توسعه داده شده و یک کامپایلر تمامعیار با همه ابزارهایی است که یک برنامهنویس به آن نیاز دارد.
- کامپایلر Clang که بر مبنای پروژه LLVM توسعه داده شده و بسیار شبیه به کامپایلر GCC است.
- کامپایلر MSVS که مخفف MicroSoft Visual Studio هست و توسط مایکروسافت توسعه داده شده است.
- …
ما در این آموزش از کامپایلر GCC استفاده میکنیم.حالا چرا؟
زیرا کامپایلر GCC یک کامپایلر Open Platform هست. یعنی این کامپایلر قابلیتش رو داره که برنامه شما رو برای هر CPU کامپایل کنه.برای مثال اگه شما برنامهای نوشته باشید و بخواهید توسط کامپایلر GCC(GCC compiler) کامپایل برنامتون رو انجام بدید، میتونید اون برنامه رو برای کامپیوتری که مثلاً سی پی یوش اینتل هست کامپایل کنید، یا برای رزبری پای که سی پی یوش آرم هست یا حتی میکروکنترلری که سی پی یوش ARM Cortex M3 هست.
شاید برای شما مفید باشد: آموزش رزبری پای از 0 تا 100
همچین دلایل مهمتری از این دلیلی که من گفتم هم وجود دارد که توصیه میکنم قبل از هر اقدامی این مطالب “کامپایلر Codevisionavr در مقابل کامپایلر GCC و مقایسه تخصصی آنها” و “مقایسه تخصصی کامپایلر کیل و GCC“مطالعه کنید.
خب حالا که یکم با GCC آشنا شدید همین اول بگم که نگید نگفتم :(کلاً کامپایلر GCC در سیستمعاملهایی که کرنل اونها بر مبنای UNIX نوشته شده باشد عملکرد بهتری دارد مثل لینوکس یا مک) اما کلاً عملکردش در ویندوز کندتره.
IDE چیست؟؟؟
بعضی نرمافزارها هستند که هم میشه داخلشون برنامه مون رو ویرایش کنیم و هم میشه با زدن یک کلید برنامه رو کامپایل کرد و هم بعد از اون میشه برنامه رو دیباگ هم کرد، مثل Arduino IDE یا STM32CubeIDE یا Keil . به این نرمافزارها IDE گفته میشود.IDE ها یک محیط یکپارچه از ابزارهای ادیتور کد، کامپایلر و دیباگر رو برای ما فراهم میکنند تا بتونیم راحتتر و بهتر برنامهنویسی کنیم. |
خوب حالا قراره بریم سراغ عملیاتیکردن حرفامون و کامپایلر gcc رو نصب کنیم و با اون کار کنیم تا پیچوخم کار تا حدودی دستمون بیاد اما قبلش باید بگم که کلاً از این به بعد ماجرا خبری از یک محیط گرافیکی قشنگ که حضرات مرحمت بفرمایند و کلید کامپایل رو بزنند و کامپایل انجام بشه نیس و ما قراره همه کارهامون رو با ترمینال لینوکس انجام بدیم. چون ترمینال دسترسیهای بیشتری برای کامپایل به ما میده و بهتر میتونیم با قابلیتهای کامپایلر آشنا بشیم. بزن بریم…
چطور کامپایلر GCC رو نصب کنیم؟
کامپایلر gcc در بعضی توزیعهای لینوکس مثل Mint بهصورت خودکار نصب هست؛ اما برای بقیه توزیعها مثل Ubuntu باید از یه یار قدیمی و همیشگی به نام دستور apt استفاده کنیم و کامپایلر gcc رو از ریپوزیتوری دریافت و نصب کنیم. به همین سادگی.
1 | sudo apt install gcc |
حالا میخوایم یه کد خیلی ساده به زبان C رو توسط این کامپایلر ، کامپایل کنیم که کد بهصورت زیر هست:
1 2 3 4 5 | //metech.c Source Code #include <stdio.h> int main(){ printf("Hello World!\n"); } |
خوب حالا میخوایم کدمون رو کامپایل کنیم یا به عبارت دیگه به یک فایل اجرایی تبدیلش کنیم، پس از دستور gcc استفاده میکنیم که ساختارش بهصورت زیر هست:
1 | gcc [options] [file] |
1 | gcc metech.c |
اگه بهصورت بالا از دستور gcc استفاده کنیم نام فایل خروجی توسط کامپایلر بهصورت پیشفرض تعیین میشود. حال برای افزودن نام فایل خروجی بهصورت زیر عمل میکنیم:
1 | gcc metech.c -o output |
دقت کنید نامی که پس از سوئیچ o- میآید،نام فایل خروجی است. خوب حالا که فایل خروجی ساخته شد، برای اجرای آن کافی است که نام فایل خروجی را در ترمینال فراخوانی کنیم:
1 | ./output |
نکته : یک فایل اجرایی در لینوکس مانند یک فایل اجرایی در ویندوز نیس که حتماً نیاز به پسوند داشته باشد. ممکنه یک فایل اجرایی در لینوکس هیچ پسوندی نداشته باشد و تنها این مهمه که اون فایل دسترسی اجراشدن داشته باشد؛ مانند فایل زیر:
1 | -rwxr-xr-x 1 metech metech 15960 Jan 28 09:39 output |
حالا میخواهیم یکم کار روسخت تر کنیم و یک هدر فایل به برنامه اضافه کنیم که در این صورت برناممون بهصورت زیر میشود:
1 2 3 4 5 6 | //metech.h #include <stdio.h> void print(void){ printf("Hello World!\nIm MEtech\n"); } |
1 2 3 4 5 | //metech.c #include "metech.h" int main(){ print(); } |
همونطور که میبینید ما فایل کتابخانه metech.h رو که در داخل دایرکتوری برناممون بود، به برنامه اضافه کردیم و از تابع print آن استفاده کردیم.حالا برنامه رو مثل قبل کامپایل میکنیم و میبینیم که اجرا میشود .
حالا سؤال اینجاست که چرا کتابخانه metech.h رو داخل کوتیشن قرار دادیم و کتابخانه stdio.h رو داخل براکت زاویهدار؟ تفاوتشون چیه؟
در برنامه نویسی برای الحاق فایل های کتابخانه به برنامه با استفاده از دستور include# دو حالت وجود دارد که این دو حالت بر عملکرد کامپایلر در پیداکردن فایل کتابخانه تاثیرگذار است:
در برنامهنویسی برای الحاق فایلهای کتابخانه به برنامه با استفاده از دستور include# دو حالت وجود دارد که این دو حالت بر عملکرد کامپایلر در پیداکردن فایل کتابخانه تأثیرگذار است:
1.Quotation Mode : در این حالت برای الحاق کتابخانه به برنامه از ” ” استفاده میکنیم و برای کامپایل اگر:
- فایل کتابخانه در دایرکتوری سورس کد قرار داشته باشد، از دستور gcc بهصورت زیر استفاده میکنیم:
1 | gcc metech.c -o output |
- فایل کتابخانه در دایرکتوری دیگری قرار داشته باشد، از دستور gcc بهصورت زیر استفاده میکنیم:
1 | gcc -iquote [Address of Header Directory] metech.c -o output |
در این حالت در واقع کامپایلر برای یافتن فایل کتابخانه به روش زیر عمل میکند:
2.Angle Bracket Mode : در این حالت برای الحاق کتابخانه به برنامه از <> استفاده میکنیم و از این روش بیشتر برای الحاق کتابخانههای استاندارد سیستم مانند stdio.h استفاده میشود.حالا اگه بخواهیم کتابخانهای که خودمون نوشتیم رو به این روش به سورس کد اضافه کنیم، از دستور gcc بهصورت زیر استفاده میکنیم:
1 | gcc -I [Address of Header Directory] metech.c -o output |
در این حالت در واقع کامپایلر برای یافتن فایل کتابخانه به روش زیر عمل میکند:
در واقع در این حالت تنها در بین کتابخانههای استاندارد سیستم جستجو انجام میشود و به همین دلیل است که بیشتر کتابخانههای از پیش تعریف شده را به این روش به برنامه الحاق میکنند.حالا محل این کتابخانههای استاندارد کجاست و شامل چه کتابخانههایی میشود؟
کتابخانههای استاندارد در واقع تعدادی از کتابخانههای زبان C هستند که توسط برنامهنویسان توسعه داده شدهاند و به آنها کتابخانههای از پیش تعریف شده نیز گفته میشود.
حالا که یک مثال ساده زدیم و کمی با کامپایلر GCC در محیط ترمینال آشنا شدیم، میخوایم جزئیتر فرایند کامپایل برنامه را دنبال کنیم. پس با ما همراه باشید.
مراحل کامپایل یک برنامه توسط کامپایلر GCC
1.پیشپردازش (PreProcessing)
این مرحله اولین مرحله از فرایند کامپایل یک برنامه است. در این مرحله دستورات پیشپردازنده با مقادیر واقعیشان جایگزین میشوند.حالا این دستورات پیشپردازنده چی هست اصلاً؟ دستورات پیشپردازنده به کلیه دستوراتی در زبان C گفته میشود که با علامت # شروع میشوند و در واقع کارشان جایگزینی یک مقدار یا فایل با یک نام است که در مرحله پیشپردازش این اسامی با مقادیر واقعیشان جایگزین میشوند. برای مثال دستورات پیشپردازنده مثال قبل:
1 | #include <stdio.h> |
1 | #include "metech.h" |
حالا میخواهیم یک مثال ساده بزنیم و فایل خروجی پیشپردازنده رو از کامپایلر استخراج کنیم تا منظور حرفم رو بهتر متوجه بشید.
1 2 3 4 5 6 7 8 9 10 11 | //metech2.h #include <stdio.h> #define METECH 10 void function1(void){ printf("fanction1 = %d\n", METECH); } void function2(void){ printf("fanction2 = %d\n", METECH); } |
1 2 3 4 5 6 7 | //metech2.c #include "metech2.h" int main(){ function1(); function2(); } |
خب پس از کامپایل خواهیم دید که برنامه بهدرستی اجرا میشود و اما برای استخراج فایل پیشپردازش شده باید از دستور gcc بهصورت زیر استفاده کنیم:
1 | gcc -E [Name of Source Code] -o [Name of Output File] |
مانند:
1 | gcc -E metech2.c -o PreProcessed.i |
معمولاً فایلهای پیشپردازش شده دارای پسوند “i.” هستند.
حال اگر پس از اجرای دستور بالا فایل خروجی پیشپردازش شده را بهصورت فایل متنی باز کنیم خواهیم دید که :
توجه:قبل از توضیح کد باید بگم در فایلهای خروجی پیشپردازنده برای کامنتگذاری از علامت # همراه با یک عدد استفاده میشود. مانند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 | # 0 "metech2.c" # 0 "<built-in>" # 0 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 0 "<command-line>" 2 # 1 "metech2.c" # 1 "metech2.h" 1 # 1 "/usr/include/stdio.h" 1 3 4 # 27 "/usr/include/stdio.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4 # 33 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 3 4 # 1 "/usr/include/features.h" 1 3 4 # 392 "/usr/include/features.h" 3 4 # 1 "/usr/include/features-time64.h" 1 3 4 # 20 "/usr/include/features-time64.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4 # 21 "/usr/include/features-time64.h" 2 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 1 3 4 # 19 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4 # 20 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 2 3 4 # 22 "/usr/include/features-time64.h" 2 3 4 # 393 "/usr/include/features.h" 2 3 4 # 486 "/usr/include/features.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4 # 559 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4 # 560 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 1 3 4 # 561 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4 # 487 "/usr/include/features.h" 2 3 4 # 510 "/usr/include/features.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 1 3 4 # 10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 1 3 4 # 11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 3 4 # 511 "/usr/include/features.h" 2 3 4 # 34 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 2 3 4 # 28 "/usr/include/stdio.h" 2 3 4 # 1 "/usr/lib/gcc/x86_64-linux-gnu/11/include/stddef.h" 1 3 4 # 209 "/usr/lib/gcc/x86_64-linux-gnu/11/include/stddef.h" 3 4 # 209 "/usr/lib/gcc/x86_64-linux-gnu/11/include/stddef.h" 3 4 typedef long unsigned int size_t; # 34 "/usr/include/stdio.h" 2 3 4 # 1 "/usr/lib/gcc/x86_64-linux-gnu/11/include/stdarg.h" 1 3 4 # 40 "/usr/lib/gcc/x86_64-linux-gnu/11/include/stdarg.h" 3 4 typedef __builtin_va_list __gnuc_va_list; # 37 "/usr/include/stdio.h" 2 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/types.h" 1 3 4 # 27 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4 # 28 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 1 3 4 # 19 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4 # 20 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 2 3 4 # 29 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4 typedef unsigned char __u_char; typedef unsigned short int __u_short; typedef unsigned int __u_int; typedef unsigned long int __u_long; typedef signed char __int8_t; typedef unsigned char __uint8_t; typedef signed short int __int16_t; typedef unsigned short int __uint16_t; typedef signed int __int32_t; typedef unsigned int __uint32_t; typedef signed long int __int64_t; typedef unsigned long int __uint64_t; typedef __int8_t __int_least8_t; typedef __uint8_t __uint_least8_t; typedef __int16_t __int_least16_t; typedef __uint16_t __uint_least16_t; typedef __int32_t __int_least32_t; typedef __uint32_t __uint_least32_t; typedef __int64_t __int_least64_t; typedef __uint64_t __uint_least64_t; typedef long int __quad_t; typedef unsigned long int __u_quad_t; typedef long int __intmax_t; typedef unsigned long int __uintmax_t; # 141 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/typesizes.h" 1 3 4 # 142 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/time64.h" 1 3 4 # 143 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4 typedef unsigned long int __dev_t; typedef unsigned int __uid_t; typedef unsigned int __gid_t; typedef unsigned long int __ino_t; typedef unsigned long int __ino64_t; typedef unsigned int __mode_t; typedef unsigned long int __nlink_t; typedef long int __off_t; typedef long int __off64_t; typedef int __pid_t; typedef struct { int __val[2]; } __fsid_t; typedef long int __clock_t; typedef unsigned long int __rlim_t; typedef unsigned long int __rlim64_t; typedef unsigned int __id_t; typedef long int __time_t; typedef unsigned int __useconds_t; typedef long int __suseconds_t; typedef long int __suseconds64_t; typedef int __daddr_t; typedef int __key_t; typedef int __clockid_t; typedef void * __timer_t; typedef long int __blksize_t; typedef long int __blkcnt_t; typedef long int __blkcnt64_t; typedef unsigned long int __fsblkcnt_t; typedef unsigned long int __fsblkcnt64_t; typedef unsigned long int __fsfilcnt_t; typedef unsigned long int __fsfilcnt64_t; typedef long int __fsword_t; typedef long int __ssize_t; typedef long int __syscall_slong_t; typedef unsigned long int __syscall_ulong_t; typedef __off64_t __loff_t; typedef char *__caddr_t; typedef long int __intptr_t; typedef unsigned int __socklen_t; typedef int __sig_atomic_t; # 39 "/usr/include/stdio.h" 2 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/types/__fpos_t.h" 1 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h" 1 3 4 # 13 "/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h" 3 4 typedef struct { int __count; union { unsigned int __wch; char __wchb[4]; } __value; } __mbstate_t; # 6 "/usr/include/x86_64-linux-gnu/bits/types/__fpos_t.h" 2 3 4 typedef struct _G_fpos_t { __off_t __pos; __mbstate_t __state; } __fpos_t; # 40 "/usr/include/stdio.h" 2 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/types/__fpos64_t.h" 1 3 4 # 10 "/usr/include/x86_64-linux-gnu/bits/types/__fpos64_t.h" 3 4 typedef struct _G_fpos64_t { __off64_t __pos; __mbstate_t __state; } __fpos64_t; # 41 "/usr/include/stdio.h" 2 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/types/__FILE.h" 1 3 4 struct _IO_FILE; typedef struct _IO_FILE __FILE; # 42 "/usr/include/stdio.h" 2 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/types/FILE.h" 1 3 4 struct _IO_FILE; typedef struct _IO_FILE FILE; # 43 "/usr/include/stdio.h" 2 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h" 1 3 4 # 35 "/usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h" 3 4 struct _IO_FILE; struct _IO_marker; struct _IO_codecvt; struct _IO_wide_data; typedef void _IO_lock_t; struct _IO_FILE { int _flags; char *_IO_read_ptr; char *_IO_read_end; char *_IO_read_base; char *_IO_write_base; char *_IO_write_ptr; char *_IO_write_end; char *_IO_buf_base; char *_IO_buf_end; char *_IO_save_base; char *_IO_backup_base; char *_IO_save_end; struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; int _flags2; __off_t _old_offset; unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock; __off64_t _offset; struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf; size_t __pad5; int _mode; char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)]; }; # 44 "/usr/include/stdio.h" 2 3 4 # 52 "/usr/include/stdio.h" 3 4 typedef __gnuc_va_list va_list; # 63 "/usr/include/stdio.h" 3 4 typedef __off_t off_t; # 77 "/usr/include/stdio.h" 3 4 typedef __ssize_t ssize_t; typedef __fpos_t fpos_t; # 133 "/usr/include/stdio.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/stdio_lim.h" 1 3 4 # 134 "/usr/include/stdio.h" 2 3 4 # 143 "/usr/include/stdio.h" 3 4 extern FILE *stdin; extern FILE *stdout; extern FILE *stderr; extern int remove (const char *__filename) __attribute__ ((__nothrow__ , __leaf__)); extern int rename (const char *__old, const char *__new) __attribute__ ((__nothrow__ , __leaf__)); extern int renameat (int __oldfd, const char *__old, int __newfd, const char *__new) __attribute__ ((__nothrow__ , __leaf__)); # 178 "/usr/include/stdio.h" 3 4 extern int fclose (FILE *__stream); # 188 "/usr/include/stdio.h" 3 4 extern FILE *tmpfile (void) __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (fclose, 1))) ; # 205 "/usr/include/stdio.h" 3 4 extern char *tmpnam (char[20]) __attribute__ ((__nothrow__ , __leaf__)) ; extern char *tmpnam_r (char __s[20]) __attribute__ ((__nothrow__ , __leaf__)) ; # 222 "/usr/include/stdio.h" 3 4 extern char *tempnam (const char *__dir, const char *__pfx) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (__builtin_free, 1))); extern int fflush (FILE *__stream); # 239 "/usr/include/stdio.h" 3 4 extern int fflush_unlocked (FILE *__stream); # 258 "/usr/include/stdio.h" 3 4 extern FILE *fopen (const char *__restrict __filename, const char *__restrict __modes) __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (fclose, 1))) ; extern FILE *freopen (const char *__restrict __filename, const char *__restrict __modes, FILE *__restrict __stream) ; # 293 "/usr/include/stdio.h" 3 4 extern FILE *fdopen (int __fd, const char *__modes) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (fclose, 1))) ; # 308 "/usr/include/stdio.h" 3 4 extern FILE *fmemopen (void *__s, size_t __len, const char *__modes) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (fclose, 1))) ; extern FILE *open_memstream (char **__bufloc, size_t *__sizeloc) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (fclose, 1))) ; # 328 "/usr/include/stdio.h" 3 4 extern void setbuf (FILE *__restrict __stream, char *__restrict __buf) __attribute__ ((__nothrow__ , __leaf__)); extern int setvbuf (FILE *__restrict __stream, char *__restrict __buf, int __modes, size_t __n) __attribute__ ((__nothrow__ , __leaf__)); extern void setbuffer (FILE *__restrict __stream, char *__restrict __buf, size_t __size) __attribute__ ((__nothrow__ , __leaf__)); extern void setlinebuf (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); extern int fprintf (FILE *__restrict __stream, const char *__restrict __format, ...); extern int printf (const char *__restrict __format, ...); extern int sprintf (char *__restrict __s, const char *__restrict __format, ...) __attribute__ ((__nothrow__)); extern int vfprintf (FILE *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg); extern int vprintf (const char *__restrict __format, __gnuc_va_list __arg); extern int vsprintf (char *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg) __attribute__ ((__nothrow__)); extern int snprintf (char *__restrict __s, size_t __maxlen, const char *__restrict __format, ...) __attribute__ ((__nothrow__)) __attribute__ ((__format__ (__printf__, 3, 4))); extern int vsnprintf (char *__restrict __s, size_t __maxlen, const char *__restrict __format, __gnuc_va_list __arg) __attribute__ ((__nothrow__)) __attribute__ ((__format__ (__printf__, 3, 0))); # 403 "/usr/include/stdio.h" 3 4 extern int vdprintf (int __fd, const char *__restrict __fmt, __gnuc_va_list __arg) __attribute__ ((__format__ (__printf__, 2, 0))); extern int dprintf (int __fd, const char *__restrict __fmt, ...) __attribute__ ((__format__ (__printf__, 2, 3))); extern int fscanf (FILE *__restrict __stream, const char *__restrict __format, ...) ; extern int scanf (const char *__restrict __format, ...) ; extern int sscanf (const char *__restrict __s, const char *__restrict __format, ...) __attribute__ ((__nothrow__ , __leaf__)); # 1 "/usr/include/x86_64-linux-gnu/bits/floatn.h" 1 3 4 # 119 "/usr/include/x86_64-linux-gnu/bits/floatn.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/floatn-common.h" 1 3 4 # 24 "/usr/include/x86_64-linux-gnu/bits/floatn-common.h" 3 4 # 1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 1 3 4 # 25 "/usr/include/x86_64-linux-gnu/bits/floatn-common.h" 2 3 4 # 120 "/usr/include/x86_64-linux-gnu/bits/floatn.h" 2 3 4 # 431 "/usr/include/stdio.h" 2 3 4 extern int fscanf (FILE *__restrict __stream, const char *__restrict __format, ...) __asm__ ("" "__isoc99_fscanf") ; extern int scanf (const char *__restrict __format, ...) __asm__ ("" "__isoc99_scanf") ; extern int sscanf (const char *__restrict __s, const char *__restrict __format, ...) __asm__ ("" "__isoc99_sscanf") __attribute__ ((__nothrow__ , __leaf__)) ; # 459 "/usr/include/stdio.h" 3 4 extern int vfscanf (FILE *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg) __attribute__ ((__format__ (__scanf__, 2, 0))) ; extern int vscanf (const char *__restrict __format, __gnuc_va_list __arg) __attribute__ ((__format__ (__scanf__, 1, 0))) ; extern int vsscanf (const char *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__format__ (__scanf__, 2, 0))); extern int vfscanf (FILE *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg) __asm__ ("" "__isoc99_vfscanf") __attribute__ ((__format__ (__scanf__, 2, 0))) ; extern int vscanf (const char *__restrict __format, __gnuc_va_list __arg) __asm__ ("" "__isoc99_vscanf") __attribute__ ((__format__ (__scanf__, 1, 0))) ; extern int vsscanf (const char *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg) __asm__ ("" "__isoc99_vsscanf") __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__format__ (__scanf__, 2, 0))); # 513 "/usr/include/stdio.h" 3 4 extern int fgetc (FILE *__stream); extern int getc (FILE *__stream); extern int getchar (void); extern int getc_unlocked (FILE *__stream); extern int getchar_unlocked (void); # 538 "/usr/include/stdio.h" 3 4 extern int fgetc_unlocked (FILE *__stream); # 549 "/usr/include/stdio.h" 3 4 extern int fputc (int __c, FILE *__stream); extern int putc (int __c, FILE *__stream); extern int putchar (int __c); # 565 "/usr/include/stdio.h" 3 4 extern int fputc_unlocked (int __c, FILE *__stream); extern int putc_unlocked (int __c, FILE *__stream); extern int putchar_unlocked (int __c); extern int getw (FILE *__stream); extern int putw (int __w, FILE *__stream); extern char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream) __attribute__ ((__access__ (__write_only__, 1, 2))); # 632 "/usr/include/stdio.h" 3 4 extern __ssize_t __getdelim (char **__restrict __lineptr, size_t *__restrict __n, int __delimiter, FILE *__restrict __stream) ; extern __ssize_t getdelim (char **__restrict __lineptr, size_t *__restrict __n, int __delimiter, FILE *__restrict __stream) ; extern __ssize_t getline (char **__restrict __lineptr, size_t *__restrict __n, FILE *__restrict __stream) ; extern int fputs (const char *__restrict __s, FILE *__restrict __stream); extern int puts (const char *__s); extern int ungetc (int __c, FILE *__stream); extern size_t fread (void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __stream) ; extern size_t fwrite (const void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __s); # 702 "/usr/include/stdio.h" 3 4 extern size_t fread_unlocked (void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __stream) ; extern size_t fwrite_unlocked (const void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __stream); extern int fseek (FILE *__stream, long int __off, int __whence); extern long int ftell (FILE *__stream) ; extern void rewind (FILE *__stream); # 736 "/usr/include/stdio.h" 3 4 extern int fseeko (FILE *__stream, __off_t __off, int __whence); extern __off_t ftello (FILE *__stream) ; # 760 "/usr/include/stdio.h" 3 4 extern int fgetpos (FILE *__restrict __stream, fpos_t *__restrict __pos); extern int fsetpos (FILE *__stream, const fpos_t *__pos); # 786 "/usr/include/stdio.h" 3 4 extern void clearerr (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); extern int feof (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ; extern int ferror (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ; extern void clearerr_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); extern int feof_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ; extern int ferror_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ; extern void perror (const char *__s); extern int fileno (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ; extern int fileno_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ; # 823 "/usr/include/stdio.h" 3 4 extern int pclose (FILE *__stream); extern FILE *popen (const char *__command, const char *__modes) __attribute__ ((__malloc__)) __attribute__ ((__malloc__ (pclose, 1))) ; extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__access__ (__write_only__, 1))); # 867 "/usr/include/stdio.h" 3 4 extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ; extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); # 885 "/usr/include/stdio.h" 3 4 extern int __uflow (FILE *); extern int __overflow (FILE *, int); # 902 "/usr/include/stdio.h" 3 4 # 2 "metech2.h" 2 # 3 "metech2.h" void fanction1(void){ printf("fanction1 = %d\n", 10); } void fanction2(void){ printf("fanction2 = %d\n", 10); } # 2 "metech2.c" 2 int main(){ fanction1(); fanction2(); } |
توجه:قبل از توضیح کد باید بگم در فایل های خروجی پیش پردازنده برای کامنت گذاری از علامت # همراه با یک عدد استفاده میشود.مانند:
1 | # 1 This Line is Comment in PreProcessed File |
خب حالا بریم سراغ توضیح فایل:
در این مثال همونطور که میبینید از خط 512 سورس کد شروع میشود.همانطور که قبلا گفتم به جای دستورات پیش پردازنده محتوای آنها جایگذاری شده است.مثلا به جای:
1 | #include "metech2.h" |
محتوای آن یعنی:
1 2 3 4 5 6 7 | void fanction1(void){ printf("fanction1 = %d\n", 10); } void fanction2(void){ printf("fanction2 = %d\n", 10); } |
جایگذاری شده است. حالا شاید بپرسید پس کتابخانه stdio.h چی شد؟ باید بگم که تا قبل از خط 512 در برنامه تمامی کدهایی که میبینید محتوای کتابخانه stdio.h هستند.
یا مثلاً بهجای ماکروی METECH که در توابع کتابخانه استفاده شده بود، مقدار اختصاصدادهشده به آن یعنی عدد 10 قرار گرفته است. یعنی این قسمت از کد:
1 2 3 4 5 6 7 | void fanction1(void){ printf("fanction1 = %d\n", METECH); } void fanction2(void){ printf("fanction2 = %d\n", METECH); } |
به این قسمت:
1 2 3 4 5 6 7 | void fanction1(void){ printf("fanction1 = %d\n", 10); } void fanction2(void){ printf("fanction2 = %d\n", 10); } |
تبدیل شده است.
نتیجهگیری
- دستورات پیشپردازنده با محتوایشان جایگزین شدهاند و خود این دستورات یا کامنت شده یا حذف شدهاند.
- در این مرحله، عملکرد کامپایلر بهصورت بازگشتی است. یعنی ابتدا کتابخانههایی که سورس کد به آنها نیاز دارد را مییابد، سپس کتابخانههایی که کتابخانههای سورس کد به آنها نیاز دارد را مییابد و این کار را تا جایی ادامه میدهد که کتابخانهای موردنیاز کتابخانه دیگر نباشد. حالا از همان نقطه شروع به جایگزینی کتابخانهها با محتوای آنها در کد میکند و میرسیم به کدی که در حال حاضر میبینید.
حالا که با مرحله پیشپردازش و فایل خروجی آن آشنا شدید، جا داره بگم که شما برای مقداردهی به ثابتهای کدتان میتونید بهجای اینکه در برنامه مقداردهی کنید هنگام کامپایل این کار رو انجام بدید تا در موقع پیشپردازش مقدار موردنظر شما در برنامه جایگذاری شود. مثلاً فرض کنید کد زیر رو دارید:
1 2 3 4 5 | //metech3.c Source Code #include <stdio.h> int main(){ printf("5 * 2 = %d", A); } |
خوب همونطور که در این کد میبینید ثابت A رو تعریف نکردیم و قطعاً اگه الان کد رو کامپایل کنیم با ارور مواجه میشیم، مگر اینکه از دستور gcc بهصورت زیر استفاده کنیم:
1 | gcc -D<Name of Constant>=<Value> [Name of Source Code] -o [Name of Output file] |
مانند:
1 | gcc -DA=100 metech3.c -o output |
خوب حالا اگه کد رو کامپایل کنید مشکلی پیش نمیاید. چرا؟ چون در مرحله پیشپردازش مقدار 10 بهجای ثابت A قرار داده میشود.
2. کامپایل (Compilling)
در این مرحله کد پیشپردازش شده کامپایل میشود. یعنی کد زبان C به یک کد اسمبلی تبدیل میشود. زبان اسمبلی یکی از زبانهای سطح پایین محسوب میشود که در آن ما معمولاً مستقیماً با رجیسترهای پردازنده درگیر هستیم و تمامی عملیات های ریاضی و منطقی خود را از طریق کار بر روی رجیسترها انجام میدهیم. حال برای دیدن کد اسمبلی خروجی میتوان از دستور gcc بهصورت زیر استفاده کرد:
1 | gcc -S [Name of Source Code] -o [Name of Output file] |
مانند:
1 | gcc -S metech2.c -o assembled.s |
معمولاً فایلهای تبدیل شده به فایل اسمبلی دارای پسوند “s.” هستند.
3. تبدیل کد اسمبلی به زبان ماشین(Creating Object File)
در این مرحله کد اسمبلی به کد زبان ماشین(Object file) تبدیل میشود.Object file ها در واقع همان کدهای صفر و یکی هستند که توسط پردازنده قابلفهم و اجراست. پس در واقع Object file ها دستورالعمل اجرایی پردازنده هستند. حال برای دریافت کد زبان ماشین از دستور gcc بهصورت زیر استفاده میکنیم:
1 | gcc -c [Name of Source Code] -o [Name of Output file] |
مانند:
1 | gcc -c metech2.c -o ObjectFile.o |
معمولاً Object File ها دارای پسوند “o.” هستند.
نکته: Object file ها را نمیتوان بهصورت یک فایل متنی باز کرد و محتوای آنها را دید چرا که یک فایل اجرایی هستند و یک فایل متنی نیستند؛ بنابراین اگر قصد دارید محتوای Object file را مشاهده کنید میتوانید از این ابزار :
1 | objdump [Option] [File] |
به این صورت استفاده کنید:
1 | objdump -D ObjectFile.o |
4. لینک کردن(Linking)
همانطور که گفتیم Object file بسیار شبیه به فایل اجرایی میباشد و تنها نکتهای که در آن وجود دارد این است که توابعی مثل printf که ما در برنامه استفاده میکنیم متأسفانه توسط Object File سورس کد قابلاجرا نیس، زیرا محتوای آن توابع نه در فایل کتابخانهای قرار دارد و نه در خود سورس کد وجود دارد. پس باید این Object file به Object file هایی که بدنه این توابع در آنها قرار دارد متصل شود تا فایل نهایی قابلیت اجرا داشته باشد. به این کار Linking گفته میشود.
جمع بندی
خلاصه مراحل کامپایل یک سورس کد:
خلاصه سوئیچهای دستور gcc برای گرفتن فایلهای خروجی مختلف(فایل پیشپردازش شده، فایل اسمبلی،Object file)
چگونه کد خود را Close Source کنیم؟
شاید براتون سؤال پیش اومده باشه که در بلوک دیاگرام مراحل کامپایل این کتابخانههای استاتیک چی هستند؟
گاهی اوقات پیش میاد که ما قصد داریم کدمون رو به برنامهنویسهای دیگه بدیم تا اونها هم به تونن از کتابخانههای ما استفاده کنن اما نمیخوابم اونها کتابخانههای ما رو تغییر بدن؛ بنابراین لازمه که اون کتابخانهها را Close source کنیم.یکی از راههای Close source کردن کد،تبدیل آن به یک Object file است که عملاً قابلیت تغییر ندارد. حالا میخوایم یک مثال بزنیم تا مسئله واضحتر بشه: فرض کنید ما کد زیر را داریم:
1 2 3 | //Mymath.h Header file int zarb(int i, int j); int taghsim(int i, int j); |
1 2 3 4 5 | //zarb.c #include "Mymath.h" int zarb(int i, int j){ return i*j; } |
1 2 3 4 5 | //taghsim.c #include "Mymath.h" int taghsim(int i, int j){ return i/j; } |
1 2 3 4 5 6 7 | //main.c Source code #include <stdio.h> #include "Mymath.h" int main(){ printf("16 * 2 = %d \n", zarb(16, 2)); printf("16 / 2 = %d \n", taghsim(16, 2)); } |
همونطور که میبینید ما یک سورس کد main داریم که در آن از توابع ضرب و تقسیم استفاده شده که این جزئی از توابع استاندارد سیستمی نیستند و خود ما اونها رو نوشتیم و در یک هدر فایل اونها رو معرفی کردیم و بدنه توابع را نیز در فایلهای کد جداگانه تعریف کردیم. حال برای بستن فایل کد کتابخانهها میتوانیم با دستور gcc آنها را به Object file تبدیل کنیم و اما حالا
چطور کامپایل را انجام دهیم؟
بهصورت زیر:
1 | gcc main.c zarb.o taghsim.o -o output |
اینطوری بهراحتی کدمون کامپایل میشه. حالا
اگه تعداد کتابخانهها یا توابع زیاد بود چیکار کنیم؟
دوراه وجود داره: اول اینکه همهٔ توابع رو در یک هدر فایل تعریف کنیم و برای آن هدر فایل یک فایل کد ایجاد کرده و بدنه توابع را هم در آن تعریف کنیم و تا حدودی مشکلمون حل میشه اما من روش نوشتن توابع در فایل کدهای جداگانه را ترجیح میدم، چرا که توسعه و عیبیابی برنامه سادهتر میشه. حالا برای اینکه بتونیم مشکل اصلی رو حل کنیم، یک راهش آرشیو کردن فایل کدهای کتابخانه است که برای این کار میتوانید از دستور زیر:
1 | ar [Option] [Name of archived File] [Files] |
به صورت زیر استفاده کنیم:
1 | ar -r MathLib.a zarb.o taghsim.o |
معمولاً کتابخانههای آرشیو شده دارای پسوند “a.” هستند.
همچنین میتوانید محتوای فایل آرشیو را با دستور زیر ببینید:
1 | ar -t MathLib.a |
حالا میتونیم با خیال راحت کدمون را کامپایل کنیم:
1 | gcc main.c MathLib.a -o output |
توجه:فقط باید توجه داشته باشید که در این حالت ترتیبی که در دستور بالا رعایت کردم حتماً باید رعایت بشه.چرا؟زیرا در این حالت کامپایلر ابتدا باید سورس کد را خوانده و نیازهای آن را تشخیص دهد و در مرحله بعد از داخل فایل آرشیو کدهای موردنیاز را استخراج کند. حال اگر ترتیب بالا را رعایت نکنیم، ابتدا کامپایلر با فایل آرشیو مواجه میشود و چون توابع ناشناخته را هنوز تشخیص نداده استخراجی صورت نمیگیرد و در مرحله بعد با ارور مواجه میشویم. (البته دقت کنید فقط در این روش کامپایلر اینگونه عمل میکند).
گاهی اوقات هم پیش میاد که کتابخانه ما در دایرکتوری دیگری قرار دارد که در این صورت برای شناساندن فایل آرشیو به کامپایلر باید ابتدا:
1.نام فایل را استاندارد کنیم:
1 | lib<Name of archived File>.a |
مانند:
1 | libMathLib.a |
2. از دستور gcc بهصورت زیر استفاده کنیم:
1 | gcc main.c -L [Address of Directory of Static Library] -l<Name of archived File> -o output |
توجه: فقط دقت داشته باشید که نام فایل آرشیو را باید بدون پیشوند lib و پسوند a. قرار دهید.
مانند
1 | gcc main.c -L ./Header/ -lMathLib -o output |
خب حالا که تا اینجا من رو همراهی کردید، باید بدونید که این فایل آرشیو همان کتابخانه استاتیک است.
(Static Library Vs Shared Library)کتابخانه استاتیک یا کتابخانه مشترک
با کتابخانههای استاتیک که آشنا شدید و اما کتابخانههای مشترک:
این کتابخانهها، کتابخانههایی هستند که در فرایند کامپایل شناسایی میشوند؛ اما پیشپردازش نمیشوند و بدیهی است که کامپایل هم نمیشوند. پس کِی استفاده میشن؟
باید گفت همزمان با اجرای برنامههایی که در اونها از کتابخانههای مشترک استفاده میشود، برنامهای به نام Dynamic Loader نیز اجرا میشود که وظیفه دارد هر کجا از برنامه که یکی از توابع کتابخانههای مشترک فراخوانی شده، آن کتابخانه را در داخل Ram قرار داده و به کپی برنامه در Ram متصل کند(Linking) تا برنامه اجرا شود.
خب ازآنجاکه کتابخانههای مشترک در برنامهنویسی میکروکنترلرها خیلی کاربرد ندارد، به تعریف آن بسنده میکنیم و اگر علاقهمند به دانستن بیشتر در مورد این کتابخانهها هستید، میتوانید به علامه دهر حضرت google مراجعه کرده و مطالب مفید بسیاری در این باره بیابید.
فوق العاده بود.
من حدود ۲۰ ساله که امبدد کار میکنم. این مطالب بقدری شیوا و رسا بیان شدن که در این ۲۰ سال من ندیدم. با این که به این مطالب واقف بودم ولی از خوندش سیر نمی شدم
عالی بود مهندس
خیلی آموزنده
آقا عالی
عالیییی .
تشکر از این مطلب بسیار خوب