পাঠ ১২ · ৫৭-এর মধ্যে · মডিউল ৩

ক্রস-কম্পাইলেশন, টুলচেইন ও বিল্ড প্রসেস

Cross-compilation, toolchains and the build process
৯ মিনিট পড়া মধ্যম · Intermediate Python সিমুলেশনসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ক্রস-কম্পাইলেশন কী ও এমবেডেড ডেভেলপমেন্টে কেন এটি প্রায় সবসময় প্রয়োজন হয়
  • প্রিপ্রসেসর, কম্পাইলার, অ্যাসেম্বলার ও লিংকার — প্রতিটি ধাপ ঠিক কী কাজ করে
  • লিংকার স্ক্রিপ্ট কীভাবে মেমরি ম্যাপের সাথে সম্পর্কিত (L06-এর সাথে সংযোগ)
  • একটি সত্যিকারের সিমুলেশন — লিংকিং ধাপে অবজেক্ট সেকশন মেমরি রেঞ্জে বসানোর গণনা

১ · ক্রস-কম্পাইলেশন কেন লাগে

একজন এমবেডেড ডেভেলপার সাধারণত কোড লেখেন একটি সাধারণ PC-তে (x86/x64 আর্কিটেকচার, উইন্ডোজ/লিনাক্স/ম্যাক), কিন্তু সেই কোড চালাতে হবে একটি সম্পূর্ণ ভিন্ন আর্কিটেকচারের মাইক্রোকন্ট্রোলারে (যেমন ARM Cortex-M, AVR, বা RISC-V — L04-এ পরিচিত পরিবারগুলো)। PC-র সাধারণ কম্পাইলার x86 ইনস্ট্রাকশন জেনারেট করে, যা মাইক্রোকন্ট্রোলারের CPU বুঝবে না। তাই একটি বিশেষ কম্পাইলার দরকার হয় যেটি PC-তে চলে কিন্তু টার্গেট চিপের জন্য বাইনারি জেনারেট করে — একেই বলা হয় ক্রস-কম্পাইলার, আর পুরো প্রক্রিয়াটি ক্রস-কম্পাইলেশন।

২ · পুরো বিল্ড পাইপলাইন

সোর্স কোড থেকে চিপে বসার উপযোগী বাইনারি পর্যন্ত পৌঁছাতে বেশ কয়েকটি স্বতন্ত্র ধাপ পার হতে হয় — প্রতিটি ধাপ একটি নির্দিষ্ট, সীমিত কাজ করে।

সোর্স main.c প্রিপ্রসেসর #include/#define কম্পাইলার .c → .s অ্যাসেম্বলার .s → .o লিংকার .o+script → .elf ফ্ল্যাশ টার্গেট চিপে লিংকার ধাপে একাধিক .o ফাইল একত্র হয় এবং লিংকার স্ক্রিপ্ট অনুযায়ী প্রতিটি সেকশন FLASH/SRAM-এ ঠিকানা পায় (নিচের সিমুলেশন)
প্রতিটি ধাপের আউটপুট পরের ধাপের ইনপুট — লিংকার ধাপেই প্রথমবার প্রকৃত মেমরি ঠিকানা নির্ধারিত হয়।

প্রিপ্রসেসর সোর্স কোডের টেক্সট-লেভেল রূপান্তর করে — #include ফাইল বসায়, #define ম্যাক্রো (যেমন L11-এর GPIO_REG) সম্প্রসারণ করে, কম্পাইলার আসল C কোড দেখার আগেই এই ধাপ শেষ হয়। কম্পাইলার সেই প্রিপ্রসেস-করা C কোড পড়ে টার্গেট আর্কিটেকচারের (যেমন ARM) অ্যাসেম্বলি ভাষায় রূপান্তর করে। অ্যাসেম্বলার সেই অ্যাসেম্বলি টেক্সটকে বাইনারি মেশিন কোডে (একটি অবজেক্ট ফাইল, .o) রূপান্তর করে — কিন্তু এখনো চূড়ান্ত ঠিকানা ঠিক হয়নি, প্রতিটি অবজেক্ট ফাইল স্বাধীনভাবে কম্পাইল হয়েছে।

লিংকার সবচেয়ে গুরুত্বপূর্ণ চূড়ান্ত ধাপ — এটি সব .o ফাইল একত্র করে, ফাংশন/ ভ্যারিয়েবল রেফারেন্স একে অপরের সাথে মেলায়, এবং একটি লিংকার স্ক্রিপ্ট (যা আসলে L06-এর মেমরি ম্যাপকেই ফাইল আকারে লেখা) অনুযায়ী প্রতিটি কোড/ডেটা সেকশনকে প্রকৃত FLASH/SRAM ঠিকানায় বসিয়ে দেয়। ফলাফল একটি .elf (বা তা থেকে বানানো .bin/.hex) ফাইল, যা অবশেষে একটি ফ্ল্যাশিং টুল দিয়ে টার্গেট চিপে লেখা হয়।

৩ · বাস্তব টুলচেইন — নাম পরিচিতি

এই কোর্স কোনো নির্দিষ্ট কমান্ড বা সিনট্যাক্স চালায় না (স্যান্ডবক্সে C কম্পাইলার নেই), কিন্তু বাস্তবে কাজ করার সময় আপনি এই সুপরিচিত, বাস্তব টুলগুলোর মুখোমুখি হবেন: GCC ARM Embedded টুলচেইন (একটি ফ্রি, ওপেন-সোর্স ক্রস-কম্পাইলার সুইট, ARM Cortex-M-এর জন্য ব্যাপকভাবে ব্যবহৃত), PlatformIO (একটি জনপ্রিয় ক্রস-প্ল্যাটফর্ম বিল্ড সিস্টেম/IDE-প্লাগইন যা একাধিক টুলচেইন ও বোর্ড সমর্থন করে), এবং Keil MDK (ARM-এর নিজস্ব কমার্শিয়াল IDE+টুলচেইন, বিশেষ করে STM32-শ্রেণির চিপে জনপ্রিয়)।

৪ · সত্যিকারের সিমুলেশন — লিংকিং ধাপে মেমরি প্লেসমেন্ট

নিচের কোড সেলে কয়েকটি অবজেক্ট ফাইলের সেকশন সাইজ (.text কোড, .data ইনিশিয়ালাইজড ভ্যারিয়েবল, .bss আন-ইনিশিয়ালাইজড ভ্যারিয়েবলের জন্য সংরক্ষিত জায়গা) এবং L06-এর ধাঁচের একটি সরল মেমরি ম্যাপ দেওয়া আছে। একটি সাধারণ বাম্প-অ্যালোকেটর (একটি cursor ক্রমাগত এগিয়ে নিয়ে পরপর জায়গা বরাদ্দ করা) দিয়ে প্রকৃতপক্ষে গণনা করে দেখানো হয়েছে প্রতিটি সেকশন কোথায় বসবে এবং সবকিছু বরাদ্দকৃত রেঞ্জে ফিট করে কিনা।

Python
# --- মেমরি ম্যাপ (L06-এর ধারণার পুনর্ব্যবহার) -- প্রতিটি অঞ্চলের শুরু ঠিকানা ও মোট আকার (বাইটে) ---
memory_regions = {
    "FLASH": {"start": 0x08000000, "size": 256 * 1024},  # .text ও .data-এর প্রাথমিক মান এখানে থাকে
    "SRAM":  {"start": 0x20000000, "size": 64 * 1024},   # .data-এর রানটাইম কপি ও .bss এখানে থাকে
}

# --- কয়েকটি অবজেক্ট ফাইলের সেকশন সাইজ (বাইটে) -- বাস্তবে কম্পাইলার/অ্যাসেম্বলার এগুলো জেনারেট করে ---
object_files = [
    {"name": "main.o",    "text": 4200, "data": 64, "bss": 128},
    {"name": "gpio.o",    "text": 1800, "data": 0,  "bss": 4},
    {"name": "uart.o",    "text": 2600, "data": 32, "bss": 256},
    {"name": "startup.o", "text": 300,  "data": 0,  "bss": 0},
]

total_text = sum(o["text"] for o in object_files)
total_data = sum(o["data"] for o in object_files)
total_bss  = sum(o["bss"]  for o in object_files)

print("=== অবজেক্ট ফাইলের সেকশন সাইজ ===")
for o in object_files:
    print(f"  {o['name']:12s} .text={o['text']:5d}B  .data={o['data']:4d}B  .bss={o['bss']:4d}B")
print(f"  {'মোট':12s} .text={total_text:5d}B  .data={total_data:4d}B  .bss={total_bss:4d}B")

def place(region_name, size, cursor):
    """সরল বাম্প-অ্যালোকেটর -- cursor থেকে শুরু করে size বাইট বরাদ্দ করে, শেষ ঠিকানা ও fits-flag ফেরত দেয়।"""
    region = memory_regions[region_name]
    start_addr = cursor
    end_addr = cursor + size
    region_end = region["start"] + region["size"]
    fits = end_addr <= region_end
    return start_addr, end_addr, fits

print()
print("=== লিংকার প্লেসমেন্ট (বাম্প-অ্যালোকেটর) ===")

# .text -- FLASH-এ, একদম শুরু থেকে
text_start, text_end, text_fits = place("FLASH", total_text, memory_regions["FLASH"]["start"])
print(f".text    FLASH  0x{text_start:08X} - 0x{text_end:08X}  ({total_text} বাইট)  fits={text_fits}")

# .data-এর ইনিশিয়াল ভ্যালু -- FLASH-এই .text-এর ঠিক পরে
data_flash_start, data_flash_end, data_flash_fits = place("FLASH", total_data, text_end)
print(f".data(F) FLASH  0x{data_flash_start:08X} - 0x{data_flash_end:08X}  ({total_data} বাইট)  fits={data_flash_fits}")

# .data-এর রানটাইম কপি -- SRAM-এ, একদম শুরু থেকে
data_sram_start, data_sram_end, data_sram_fits = place("SRAM", total_data, memory_regions["SRAM"]["start"])
print(f".data(R) SRAM   0x{data_sram_start:08X} - 0x{data_sram_end:08X}  ({total_data} বাইট)  fits={data_sram_fits}")

# .bss -- SRAM-এ, .data-এর রানটাইম কপির ঠিক পরে (ইনিশিয়াল ভ্যালু ছাড়া, শুধু জায়গা)
bss_start, bss_end, bss_fits = place("SRAM", total_bss, data_sram_end)
print(f".bss     SRAM   0x{bss_start:08X} - 0x{bss_end:08X}  ({total_bss} বাইট)  fits={bss_fits}")

print()
all_fit = text_fits and data_flash_fits and data_sram_fits and bss_fits
print(f"=== সবকিছু বরাদ্দকৃত মেমরি রেঞ্জের মধ্যে ফিট করলো: {all_fit} ===")

flash_used = data_flash_end - memory_regions["FLASH"]["start"]
sram_used = bss_end - memory_regions["SRAM"]["start"]
flash_pct = 100 * flash_used / memory_regions["FLASH"]["size"]
sram_pct = 100 * sram_used / memory_regions["SRAM"]["size"]
print(f"FLASH ব্যবহার: {flash_used} / {memory_regions['FLASH']['size']} বাইট ({flash_pct:.2f}%)")
print(f"SRAM  ব্যবহার: {sram_used} / {memory_regions['SRAM']['size']} বাইট ({sram_pct:.2f}%)")

    
লক্ষ্য করুন .data সেকশন দুইবার বরাদ্দ পেয়েছে — একবার FLASH-এ (ইনিশিয়াল ভ্যালুর স্থায়ী কপি, কারণ FLASH পাওয়ার-অফ হলেও মান ধরে রাখে) এবং একবার SRAM-এ (রানটাইম কপি, যেখানে প্রোগ্রাম আসলে ভ্যারিয়েবল পড়ে/লেখে)। বাস্তবে স্টার্টআপ কোড (startup.o) চিপ পাওয়ার-অন হওয়ার পরপরই FLASH থেকে SRAM-এ এই কপি করে — এটাই কেন একটি লিংকার স্ক্রিপ্ট .data-এর জন্য দুটো আলাদা ঠিকানা (LMA ও VMA) ট্র্যাক করে, ধারণাটি এখানে সরলীকৃতভাবে দেখানো হলো।
মূল কথা · Key takeaway

সোর্স কোড থেকে চিপে চলা বাইনারি পর্যন্ত পথটি একটি একক ধাপ নয়, বরং কয়েকটি স্বতন্ত্র, ভালোভাবে-সংজ্ঞায়িত ধাপের শৃঙ্খল — এবং লিংকার ধাপেই প্রথমবার L06-এর মেমরি ম্যাপ বাস্তবে প্রয়োগ হয়, প্রতিটি ফাংশন ও ভ্যারিয়েবল একটি প্রকৃত ঠিকানা পায়। এই একই ঠিকানাগুলোই পরে L11-এর পয়েন্টার dereference দিয়ে অ্যাক্সেস করা হয়। পরের পাঠে (L13) দেখা যাবে চিপে বসানোর পর সেই বাইনারি কীভাবে ডিবাগ করা হয়, যখন সাধারণ print-এর মতো সহজ টুল পাওয়া যায় না।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ কোড সেলে .bss সেকশনের জন্য FLASH-এ কোনো জায়গা বরাদ্দ করা হয়নি কেন, শুধু SRAM-এ?

.bss হলো আন-ইনিশিয়ালাইজড (বা শূন্যে ইনিশিয়ালাইজড) গ্লোবাল/স্ট্যাটিক ভ্যারিয়েবলের জন্য সংরক্ষিত জায়গা — এদের কোনো নির্দিষ্ট প্রাথমিক মান সংরক্ষণ করার দরকার নেই (স্টার্টআপ কোড শুধু পুরো অঞ্চলটি শূন্য দিয়ে ভরে দেয়)। তাই FLASH-এ এর জন্য কোনো ডেটা সংরক্ষণ করার প্রয়োজন নেই — শুধু SRAM-এ "এই জায়গাটা সংরক্ষিত থাকবে" এটুকু জানলেই চলে, যা মূল্যবান FLASH স্পেস বাঁচায়।

প্র ০২ যদি total_text এত বড় হতো যে text_fits False আসত, বাস্তব জীবনে এর অর্থ কী এবং সমাধান কী হতে পারে?

বাস্তবে লিংকার এই পরিস্থিতিতে একটি স্পষ্ট এরর দেয় (সাধারণত "region FLASH overflowed" জাতীয় বার্তা) — অর্থাৎ প্রোগ্রামের কম্পাইল-করা কোড চিপের FLASH-এর চেয়ে বড়, তাই বাইনারিই তৈরি হবে না। সমাধান হতে পারে কোড অপ্টিমাইজ করে ছোট করা (কম্পাইলার ফ্ল্যাগ পরিবর্তন, অপ্রয়োজনীয় লাইব্রেরি বাদ দেওয়া), অথবা বেশি FLASH-যুক্ত একটি বড় চিপ ভ্যারিয়েন্টে স্থানান্তর করা।

প্র ০৩ প্রিপ্রসেসর ধাপ (#include/#define) কম্পাইলার ধাপের আগেই কেন সম্পন্ন হয়, কম্পাইলারের ভেতরে একসাথে নয়?

প্রিপ্রসেসর বিশুদ্ধভাবে টেক্সট-লেভেল প্রতিস্থাপন করে — এটি C-এর সিনট্যাক্স বা টাইপ বোঝে না, শুধু স্ট্রিং প্রতিস্থাপন ও ফাইল বসানো করে। এই আলাদা, সরল ধাপটি আগে সম্পন্ন করে ফেললে কম্পাইলার একটি সম্পূর্ণ, বিশুদ্ধ C সোর্স টেক্সট পায় যার উপর সে সিনট্যাক্স পার্সিং ও টাইপ-চেকিং করতে পারে — দুটো ভিন্ন ধরনের কাজকে আলাদা রাখাই ঐতিহাসিকভাবে সহজ ও নির্ভরযোগ্য প্রমাণিত হয়েছে।

অনুশীলন

  1. চিন্তা করুন: যদি object_files-এ একটি নতুন sensor.o ( text=1500, data=8, bss=32) যোগ করা হয়, total_text, total_data ও total_bss-এর নতুন মান কত হবে বলে আপনার ধারণা?

    বর্তমান মোট ছিল total_text=8900, total_data=96, total_bss=388 — নতুন ফাইল যোগ করলে এগুলো হবে যথাক্রমে $8900+1500=10400$, $96+8=104$, এবং $388+32=420$ বাইট।

  2. পরীক্ষা করুন: কোড সেলে memory_regions["FLASH"]["size"]-এর মান 256 * 1024 থেকে কমিয়ে 8 * 1024 (মাত্র ৮ KB) করে Run চেপে দেখুন text_fits ও all_fit-এর মান কীভাবে বদলায়।

    ৮ KB = ৮১৯২ বাইট, কিন্তু total_text-ই একাই ৮৯০০ বাইট — তাই .text নিজেই FLASH-এর নতুন, ছোট আকারের চেয়ে বড়, ফলে text_fits=False দেখাবে এবং যেহেতু .data(F)-ও একই (এখন ওভারফ্লো হওয়া) region-এর উপর নির্ভরশীল, all_fitও False হয়ে যাবে — বাস্তবে এটিই লিংকারের "region overflowed" এরর-এর সমতুল্য।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ মাইক্রোপ্রসেসর আর্কিটেকচার, এমবেডেড C, GPIO, টাইমার/PWM/ADC, সিরিয়াল প্রোটোকল, RTOS, সেন্সর/অ্যাকচুয়েটর, IoT আর্কিটেকচার, ওয়্যারলেস প্রোটোকল, MQTT/CoAP ও IoT সিকিউরিটি — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • Computer Architecture & Digital Logic কোর্স সহোদর কোর্স সাধারণ কম্পাইলেশন/অ্যাসেম্বলি ধারণার ভিত্তি সেই কোর্সেই তৈরি হয়েছে — এই কোর্স ক্রস-কম্পাইলেশনের এমবেডেড-নির্দিষ্ট বাস্তবতায় গভীরে যায়।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Design and Analysis of Algorithms ও আরও অনেক কোর্স — সব এক জায়গায়।
আগের পাঠ
C-তে পয়েন্টার ও মেমরি-ম্যাপড রেজিস্টার