সরাসরি পর্বে যান
পর্ব ৩০ · ৬৪-এর মধ্যে · মডিউল ৫

মেমরির ভিত্তি — address binding, logical আর physical address

Memory Basics — Protection, Address Binding, Logical vs Physical Addresses, Linking, Loading and Swapping

Taught in Bangla, this lesson explains why memory management is needed, how base and limit registers protect memory with hand calculations, the three times of address binding, logical versus physical addresses and the MMU relocation register. It traces a printf call through compile, link and load using nm, readelf and ldd, covers static and dynamic linking, PLT/GOT, dlopen, position-independent code, ASLR, swapping costs and overlays.

এই পর্বে যা শিখবেন

  • মেমরি ম্যানেজমেন্ট কেন লাগে, আর CPU কেন শুধু register আর main memory সরাসরি ছুঁতে পারে
  • Base ও limit register দিয়ে হার্ডওয়্যার কীভাবে প্রতিটি ঠিকানা পাহারা দেয় — হাতে হিসাবসহ
  • Address binding-এর তিন সময়, logical ও physical address, MMU আর relocation register-এর অঙ্ক
  • Compile, link আর load-এর পথে একটি printf কলের যাত্রা — nm, readelf, ldd দিয়ে নিজের চোখে; static ও dynamic linking, PLT/GOT, dlopen, PIC আর ASLR
  • Swapping-এর খরচ হিসাব, মোবাইল OS কেন ক্লাসিক swap এড়িয়ে চলে, আর overlay-র ইতিহাস

১ · মেমরি ম্যানেজমেন্ট কেন — এক লাইব্রেরি, অনেক পাঠক

একটি পাবলিক লাইব্রেরির পড়ার ঘর ভাবুন। টেবিল সীমিত, পাঠক অনেক। লাইব্রেরিয়ানকে কয়েকটি কাজ করতে হয়: প্রত্যেককে একটি জায়গা দেওয়া; কেউ যেন পাশের জনের খাতা-বই নিয়ে না নেয়; কেউ উঠে গেলে তার জায়গা অন্য কাউকে দেওয়া; আর একটি দামি অভিধান সবাই যেন ভাগ করে ব্যবহার করতে পারে, প্রত্যেকের জন্য আলাদা কপি না কিনে। কম্পিউটারের RAM ঠিক এই পড়ার ঘর, আর লাইব্রেরিয়ান হলো OS-এর memory management অংশ।

পর্ব ৮-এ (প্রসেসের memory layout) দেখেছেন, প্রতিটি প্রসেসের নিজের text, data, BSS, heap আর stack আছে। একটি সাধারণ ল্যাপটপে একসাথে কয়েকশো প্রসেস থাকে। সবার মেমরি একই RAM চিপে। তাই চারটি প্রশ্নের উত্তর OS আর হার্ডওয়্যারকে মিলে দিতে হয়:

🧩 জায়গা দেওয়া (allocation)

কোন প্রসেস RAM-এর কোন অংশ পাবে, আর শেষ হলে সেই জায়গা কীভাবে ফেরত আসবে। Multiprogramming-এর পুরো লাভ (পর্ব ২) নির্ভর করে একসাথে কয়টি প্রসেস RAM-এ রাখা যায় তার ওপর।

কে কোথায় বসবে

🛡️ সুরক্ষা (protection)

একটি প্রসেস যেন অন্যের বা kernel-এর মেমরি পড়তে বা নষ্ট করতে না পারে। এটি প্রতিটি মেমরি-ব্যবহারে যাচাই করতে হয়, তাই কাজটি সফটওয়্যারে নয়, হার্ডওয়্যারে হয়।

পাশের জনের খাতা নয়

🚚 স্থানান্তর (relocation)

প্রোগ্রাম লেখার সময় কেউ জানে না সেটি RAM-এর কোন ঠিকানায় বসবে। আজ ১০০০০-এ, কাল ৫০০০০-এ। এমনকি চলার মাঝেও OS প্রসেসকে সরাতে চাইতে পারে।

যেকোনো টেবিলে বসা যায়

🤝 ভাগাভাগি (sharing)

দুইশো প্রসেস যদি একই C library ব্যবহার করে, RAM-এ তার দুইশো কপি রাখা অপচয়। নিয়ন্ত্রিতভাবে ভাগ করার ব্যবস্থাও তাই memory management-এর কাজ।

এক অভিধান, অনেক পাঠক

CPU সরাসরি কী ছুঁতে পারে

একটি মৌলিক সত্য দিয়ে শুরু করা যাক: CPU সরাসরি পড়তে-লিখতে পারে শুধু নিজের register আর main memory. Machine instruction-এর argument হিসেবে মেমরি-ঠিকানা দেওয়া যায়, কিন্তু ডিস্কের ঠিকানা দেওয়া যায় না। ডিস্ক বা SSD-র ডেটা আগে I/O করে RAM-এ আনতে হয় (পর্ব ৩৯), তারপরই CPU তা ব্যবহার করতে পারে। তাই চলমান প্রোগ্রামের কোড আর যে ডেটা নিয়ে সে কাজ করছে, দুটোই RAM-এ থাকতে হবে।

গতির পার্থক্যও বড়। Register একটি CPU cycle-এর মধ্যেই পাওয়া যায়, অথচ RAM থেকে একটি মান আনতে অনেক cycle লাগতে পারে — মাঝের এই ফাঁক ঢাকতে cache থাকে। এই memory hierarchy আর cache-এর বিস্তারিত পর্ব ৪-এ (memory hierarchy) দেখেছেন; এখানে শুধু মনে রাখুন: সুরক্ষা-যাচাইটি প্রতিটি মেমরি-ব্যবহারে হতে হবে, আর তাতে বাড়তি সময় প্রায় লাগা চলবে না. প্রতিবার kernel-কে ডেকে জিজ্ঞেস করা অসম্ভব — সেটি হাজার গুণ ধীর হবে। এ কারণেই নিচের সব কৌশলের কেন্দ্রে আছে ছোট, দ্রুত হার্ডওয়্যার।

মূল ধারণা

Memory management হলো নীতি আর যন্ত্রের যৌথ কাজ: নীতি (কাকে কোথায় কতটুকু দেওয়া হবে) ঠিক করে OS, আর যন্ত্র (প্রতিটি ঠিকানা যাচাই ও অনুবাদ) চালায় CPU-র হার্ডওয়্যার। এই মডিউলের (পর্ব ৩০–৩৮) প্রতিটি কৌশল — contiguous allocation, paging, TLB, segmentation, virtual memory — এই দুইয়ের ভাগাভাগির নতুন নতুন রূপ।

২ · Base আর limit register — হার্ডওয়্যারের দারোয়ান

সুরক্ষার সবচেয়ে সরল ব্যবস্থা দিয়ে শুরু করি। ধরা যাক প্রতিটি প্রসেস RAM-এর একটি টানা (contiguous) অংশ পায়। CPU-তে দুটি বিশেষ register থাকে:

  • Base register — প্রসেসের এলাকার সবচেয়ে ছোট বৈধ physical ঠিকানা।
  • Limit register — এলাকার আকার (কত বাইট)।

বৈধ ঠিকানা তাহলে base থেকে base + limit − 1 পর্যন্ত। User mode-এ প্রসেস যতবার মেমরিতে যায় — instruction আনতে, ডেটা পড়তে বা লিখতে — হার্ডওয়্যার দুটি তুলনা করে:

Base-limit দিয়ে হার্ডওয়্যারের ঠিকানা-যাচাই

Hardware address protection with base and limit registers

             base                 base + limit
               |                       |
 CPU -- addr --+--> addr >= base ? --yes--> addr < base+limit ? --yes--> memory
                         |                           |
                         no                          no
                         v                           v
                  trap to OS: addressing error (Linux: SIGSEGV)
ছবি ৩০.১ — দুটি তুলনার যেকোনোটি ব্যর্থ হলে মেমরিতে যাওয়ার আগেই trap. অর্থাৎ নিয়মটি: addr < base অথবা addr ≥ base + limit হলে trap.

একটি আবাসিক ভবনের দারোয়ান ভাবুন, যার খাতায় লেখা “৩ নম্বর ফ্ল্যাটের অতিথি শুধু ৩০১ থেকে ৩০৯ নম্বর ঘরে যেতে পারবেন”। অতিথি যেকোনো ঘরের নম্বর বলুন, দারোয়ান দুটি প্রশ্ন করেন: নম্বরটি ৩০১-এর কম কি? ৩১০ বা তার বেশি কি? যেকোনো একটির উত্তর “হ্যাঁ” হলে ঢোকা নিষেধ, আর খবর যায় ভবনের ম্যানেজারের কাছে। খাতাটি বদলাতে পারেন শুধু ম্যানেজার — অতিথি নিজে নয়। এখানে দারোয়ান = হার্ডওয়্যারের তুলনা, খাতা = base আর limit, ম্যানেজার = kernel.

দুটি সূক্ষ্ম কিন্তু জরুরি কথা। প্রথম, register দুটি কে বসায়? শুধু kernel, কারণ base আর limit বসানোর নির্দেশগুলো privileged instruction — user mode-এ চালালে নিজেই trap হয় (পর্ব ৪, privilege mode). প্রসেস যদি নিজের limit বাড়াতে পারত, পুরো ব্যবস্থাই অর্থহীন হতো। Context switch-এর সময় (পর্ব ৯) dispatcher নতুন প্রসেসের PCB থেকে তার base আর limit পড়ে register-এ বসায়। দ্বিতীয়, kernel নিজে কি আটকা পড়ে? না। Kernel mode-এ এই যাচাই সাধারণত বন্ধ থাকে, যাতে OS যেকোনো প্রসেসের মেমরিতে ডেটা কপি করতে পারে (যেমন read()-এর ফল ব্যবহারকারীর buffer-এ রাখা), বা প্রসেসকে ডিস্কে সরাতে পারে।

হাতে হিসাব — কোন ঠিকানা বৈধ

পাঠ্যবইয়ের একটি প্রচলিত উদাহরণ নিই: base = 300040, limit = 120900. তাহলে base + limit = 300040 + 120900 = 420940, আর বৈধ পরিসর ৩০০০৪০ থেকে ৪২০৯৩৯ (৪২০৯৪০ নিজে বৈধ নয়, কারণ শর্ত “≥ base + limit হলে trap”). নিচের ডায়াগ্রামে তিনটি ঠিকানা যাচাই হতে দেখুন:

1kernel: base = 300040; limit = 1209002CPU (user mode) wants address A3if (A < base)          -> trap4if (A >= base + limit) -> trap5else -> go to memory6trap: kernel handles addressing error
প্রতিটি ধাপে
A
A < 300040 ?
A ≥ 420940 ?
ফল
  1. প্রস্তুতি। Context switch-এ kernel এই প্রসেসের PCB থেকে base = ৩০০০৪০ আর limit = ১২০৯০০ register-এ বসাল (privileged নির্দেশ), তারপর user mode-এ ফিরল। এখন থেকে প্রতিটি ঠিকানা হার্ডওয়্যার যাচাই করবে।
  2. ঠিকানা ৩৫০০০০ — প্রথম তুলনা। ৩৫০০০০ কি ৩০০০৪০-এর কম? না। তাই নিচের সীমা পার।
  3. ৩৫০০০০ — দ্বিতীয় তুলনা। ৩৫০০০০ কি ৪২০৯৪০ বা তার বেশি? না। দুটি পরীক্ষাই পাশ, তাই মেমরিতে যাওয়ার অনুমতি।
  4. ঠিকানা ৪২০৯৪০ — নিচের সীমা ঠিক। ৩০০০৪০-এর কম নয়, তাই প্রথম পরীক্ষা পাশ। এবার দ্বিতীয়টি দেখা হবে।
  5. ৪২০৯৪০ ≥ ৪২০৯৪০ — TRAP. সমান হলেও অবৈধ! এলাকার শেষ বৈধ বাইট ৪২০৯৩৯. পরীক্ষায় এই “এক বেশি” ভুলটাই সবচেয়ে বেশি হয়। CPU মেমরিতে না গিয়ে kernel-এ trap করে।
  6. ঠিকানা ২৫৬০০০ — প্রথম তুলনাতেই TRAP. ২৫৬০০০ < ৩০০০৪০, তাই নিচের সীমা ভেঙেছে। দ্বিতীয় তুলনা দেখার দরকারই নেই। হয়তো এটি অন্য কোনো প্রসেস বা kernel-এর এলাকা — ঠিক যা আটকানো দরকার।
  7. Trap-এর পর। Kernel-এর trap handler বিষয়টিকে মারাত্মক ত্রুটি (addressing error) হিসেবে ধরে। Linux-এ প্রসেস পায় SIGSEGV — handler না থাকলে “Segmentation fault” লিখে বন্ধ হয়; Windows-এ একই ঘটনার নাম access violation.

এই ব্যবস্থার সীমাও এখনই চোখে পড়ে। প্রসেসকে RAM-এর একটি টানা অংশ দিতে হয়, আর বিভিন্ন আকারের প্রসেস আসা-যাওয়া করলে মাঝে ছোট ছোট ফাঁকা জায়গা থেকে যায় — fragmentation. এটি আর তার সমাধানের কৌশল (first fit, best fit, buddy system) পর্ব ৩১-এর বিষয়। আর একটি প্রসেসের ভেতরে কোড শুধু-পড়ার আর ডেটা পড়া-লেখার — এমন আলাদা অনুমতি base-limit দিতে পারে না; সেজন্য পরে আসবে paging (পর্ব ৩২) আর segmentation (পর্ব ৩৪)।

৩ · Address binding — ঠিকানা কখন পাকা হয়

আপনি C-তে লিখলেন count = count + 1;. এখানে count একটি নাম — CPU নাম বোঝে না, বোঝে ঠিকানা। এই নাম থেকে আসল মেমরি-ঠিকানায় পৌঁছাতে কয়েকটি ধাপ পার হয়। Compiler নামটিকে বলে “এই object file-এর data section-এর শুরু থেকে ১৬ বাইট দূরে”; linker বলে “executable-এর শুরু থেকে 0x4010-এ”; শেষে কেউ একজন বলে “RAM-এর অমুক ঘরে”। এক ধরনের ঠিকানা থেকে আরেক ধরনে এই মেলানোর কাজকে বলে address binding. মূল প্রশ্ন: শেষ মেলানো, অর্থাৎ physical ঠিকানা ঠিক করা, কখন হয়? উত্তর তিন রকম হতে পারে।

কখনকে ঠিক করেফলসুবিধাঅসুবিধা
Compile timeCompiler / linkerAbsolute code — ঠিকানাগুলো সরাসরি physicalলোড করা সবচেয়ে সহজ, বাড়তি হার্ডওয়্যার লাগে নাশুরুর ঠিকানা বদলালে আবার compile; একসাথে অনেক প্রসেস রাখা কঠিন
Load timeLoaderRelocatable code — আপেক্ষিক ঠিকানা আর ঠিক করার তালিকাপ্রতিবার যেখানে জায়গা আছে সেখানে বসানো যায়লোড হয়ে গেলে আর সরানো যায় না; লোড করতে সময় লাগে
Execution timeহার্ডওয়্যার (MMU), প্রতিটি ব্যবহারেপ্রোগ্রাম শুধু logical ঠিকানা দেখেচলার মাঝেও প্রসেস সরানো, swap করা, ভাগ করা যায়বিশেষ হার্ডওয়্যার লাগে

ঢাকা থেকে রাজশাহীর বাসের টিকিট ভাবুন। Compile time: অনলাইনে টিকিট কাটার সময়ই সিট “B3” ছাপা হয়ে গেল — বাসে উঠে ঠিক B3-তেই বসতে হবে, B3 ভাঙা থাকলে নতুন টিকিট লাগবে। Load time: টিকিটে শুধু লেখা “জানালার পাশে একটি সিট”, কাউন্টারের লোক বাসে ওঠার সময় যেটি খালি পেলেন সেটি লিখে দিলেন — যাত্রা শুরুর পর আর বদলানো যায় না। Execution time: আপনি সবসময় বলেন “আমার সিট”, আর সুপারভাইজার পথে যেকোনো স্টপেজে আপনাকে অন্য সিটে সরাতে পারেন; আপনার কাছে “আমার সিট” কথাটি একই থাকে, আসল সিট বদলায় সুপারভাইজারের খাতায়।

বাস্তবের উদাহরণ

Compile time — MS-DOS-এর .COM প্রোগ্রাম। পাঠ্যবইয়ের প্রিয় উদাহরণ। .COM ফাইলে কোনো header বা relocation তালিকা নেই; machine code ধরে নেয় সে নিজের segment-এর offset 0x100-এ শুরু হবে (আগের ২৫৬ বাইটে DOS একটি তথ্য-ব্লক রাখে)। পুরো প্রোগ্রাম একটি ৬৪ KB segment-এ আঁটতে হয়। একটি সূক্ষ্মতা: x86-এর real mode-এ segment register থাকায় DOS আসলে segment-টি যেকোনো জায়গায় বেছে নিতে পারত, তাই কঠোর অর্থে offset-গুলোই compile time-এ পাকা, পুরো physical ঠিকানা নয়। আরও খাঁটি উদাহরণ হলো boot sector-এর কোড (পর্ব ৭), যা সবসময় physical ঠিকানা 0x7C00-এ বসে, আর ছোট microcontroller-এর firmware, যা flash-এর নির্দিষ্ট ঠিকানায় লেখা হয়।

Load time — DOS-এর .EXE. .EXE ফাইলের (MZ ফরম্যাট) header-এ একটি relocation table থাকে — “এই এই জায়গায় segment-এর মান আছে, লোড করার সময় আসল segment যোগ করে দিতে হবে”। DOS প্রোগ্রাম লোড করার সময় সেগুলো ঠিক করে দিত। আজও একটি জায়গায় load-time ধারণা জীবিত: Linux-এর dynamic linker shared library লোড করার পর তার কিছু ঠিকানা (GOT) পূরণ করে — অংশ ৬-এ দেখবেন।

Execution time — আজকের প্রায় সব OS. Linux, Windows, macOS, Android, iOS — সবাই প্রতিটি মেমরি-ব্যবহারে MMU দিয়ে অনুবাদ করে। এর জন্যই একটি প্রসেস ডিস্কে swap হয়ে আবার RAM-এর সম্পূর্ণ অন্য জায়গায় ফিরতে পারে, আর প্রসেস কিছুই টের পায় না।

একটি প্রচলিত ভুল ধারণা: “relocatable code মানেই যেকোনো জায়গায় চলে”। না — relocatable code-কে কাউকে ঠিক করে দিতে হয় (loader বা linker), তবেই চলে। যে কোড কোনো বদল ছাড়াই যেকোনো ঠিকানায় চলে, তার নাম position-independent code (অংশ ৭)। আর execution-time binding-এ কোড বদলায়ই না; বদলায় শুধু MMU-র register.

৪ · Logical বনাম physical address — আর MMU

Execution-time binding থেকে একটি গুরুত্বপূর্ণ ভাগ আসে। CPU-তে চলা প্রোগ্রাম যে ঠিকানা তৈরি করে, তাকে বলে logical address (অনেক বইয়ে virtual address; এই মডিউলে দুটি একই অর্থে)। মেমরি-হার্ডওয়্যার যে ঠিকানা পায় — RAM-এর আসল ঘরের নম্বর — তা physical address. মাঝখানে বসে অনুবাদ করে MMU (memory management unit), যা আজ CPU চিপেরই অংশ।

  • একটি প্রোগ্রাম যত logical ঠিকানা তৈরি করতে পারে, তাদের সেট logical address space; সেগুলোর অনুবাদ করা physical ঠিকানার সেট physical address space.
  • Compile-time আর load-time binding-এ logical আর physical ঠিকানা একই — অনুবাদের কিছু নেই।
  • Execution-time binding-এ দুটি আলাদা. প্রোগ্রাম কখনো physical ঠিকানা দেখে না; একটি পয়েন্টার ছাপলে আপনি logical ঠিকানাই দেখেন।

সবচেয়ে সরল MMU হলো আগের base register-এর একটু ভিন্ন রূপ, যার নাম relocation register. নিয়ম: প্রোগ্রামের প্রতিটি logical ঠিকানার সাথে relocation register-এর মান যোগ হয়, তারপর মেমরিতে যায়। পাঠ্যবইয়ের বিখ্যাত উদাহরণ: relocation register = ১৪০০০ হলে logical ঠিকানা ৩৪৬ যায় physical 14346-এ। Limit register তখন logical ঠিকানার সাথে তুলনা হয় — আগে যাচাই, তারপর যোগ:

Relocation আর limit register দিয়ে MMU

Dynamic relocation: check against limit, then add the relocation register

                 limit = 3000          relocation = 14000
                      |                        |
 CPU -- logical 346 --+--> 346 < 3000 ? --yes--> (+) --> physical 14346 --> memory
                               |
                               no
                               v
                       trap: addressing error
ছবি ৩০.২ — প্রোগ্রামের চোখে তার ঠিকানা ০ থেকে ২৯৯৯; RAM-এর চোখে ১৪০০০ থেকে ১৬৯৯৯. প্রোগ্রাম ১৪০০০ সংখ্যাটি জানেই না।

অংশ ২ আর এখানকার পার্থক্যটি পরীক্ষায় গুলিয়ে যায়, তাই আলাদা করে মনে রাখুন। অংশ ২-এর base-limit: CPU-র দেওয়া ঠিকানাই physical; হার্ডওয়্যার শুধু যাচাই করে (base ≤ A < base + limit). এখানকার relocation-limit: CPU-র দেওয়া ঠিকানা logical; আগে A < limit যাচাই, তারপর A + relocation. দুটিতেই register বসায় শুধু kernel.

এখন একটি ছোট সিমুলেটরে পুরো ব্যাপারটি চালিয়ে দেখুন। দুটি প্রসেস P1 আর P2-এর relocation ও limit তাদের PCB-তে রাখা আছে। Context switch-এ kernel সেই মান MMU-তে বসায়। মাঝে kernel P1-কে RAM-এর অন্য জায়গায় সরায় — দেখুন P1-এর জন্য কী কী বদলাতে হয়:

// each process's saved registers (kept in its PCB by the kernel)
const pcb = {
  P1: { reloc: 14000, limit: 3000 },
  P2: { reloc: 20000, limit: 5000 },
};
let mmu = null;                          // what is loaded in the MMU right now
function load(p) { mmu = { name: p, ...pcb[p] }; }     // privileged: kernel only
function translate(logical) {
  if (logical >= mmu.limit)              // hardware check first ...
    return `TRAP  (${logical} >= limit ${mmu.limit}) -> kernel handles it`;
  return `physical ${logical + mmu.reloc}`;            // ... then add relocation
}
const trace = [
  ['switch', 'P1'], ['use', 0], ['use', 346], ['use', 2999], ['use', 3000],
  ['switch', 'P2'], ['use', 346], ['use', 4500],
  ['move', 'P1', 30000],                 // kernel copies P1 elsewhere (e.g. compaction)
  ['switch', 'P1'], ['use', 346],
];
for (const [op, a, b] of trace) {
  if (op === 'switch') {
    load(a);
    console.log(`-- switch to ${a}: MMU relocation=${mmu.reloc}, limit=${mmu.limit}`);
  } else if (op === 'move') {
    pcb[a].reloc = b;
    console.log(`-- kernel moves ${a} to ${b}; only the value in its PCB changes`);
  } else {
    console.log(`${mmu.name} logical ${String(a).padStart(4)} -> ${translate(a)}`);
  }
}

আসল আউটপুট:

-- switch to P1: MMU relocation=14000, limit=3000
P1 logical    0 -> physical 14000
P1 logical  346 -> physical 14346
P1 logical 2999 -> physical 16999
P1 logical 3000 -> TRAP  (3000 >= limit 3000) -> kernel handles it
-- switch to P2: MMU relocation=20000, limit=5000
P2 logical  346 -> physical 20346
P2 logical 4500 -> physical 24500
-- kernel moves P1 to 30000; only the value in its PCB changes
-- switch to P1: MMU relocation=30000, limit=3000
P1 logical  346 -> physical 30346

তিনটি জিনিস লক্ষ করুন। এক, P1-এর বৈধ logical ঠিকানা ০ থেকে ২৯৯৯; ৩০০০ ঠিক limit-এর সমান বলে trap — অংশ ২-এর “এক বেশি” নিয়ম এখানেও। দুই, P1 আর P2 দুজনেই logical ঠিকানা 346 ব্যবহার করল, কিন্তু physical ঠিকানা আলাদা (14346 আর 20346) — প্রতিটি প্রসেসের নিজস্ব ঠিকানা-জগৎ, আর কেউ অন্যের এলাকায় পৌঁছায় না। তিন, kernel P1-কে ৩০০০০-এ সরানোর পর P1-এর কোডের একটি বাইটও বদলাতে হয়নি; বদলেছে শুধু PCB-র একটি সংখ্যা। পরের বার P1 চলতেই একই logical 346 গেল 30346-এ। এটিই execution-time binding-এর শক্তি — load-time binding-এ এভাবে সরানো যেত না। trace তালিকায় নিজের ঘটনা যোগ করে দেখুন, যেমন ['use', 5000] P2-এর জন্য।

পরীক্ষার হিসাব — logical address space-এর আকার

আরেক ধরনের সহজ কিন্তু প্রচলিত প্রশ্ন: “logical ঠিকানা ১৬ bit হলে logical address space কত বড়?” উত্তর ২১৬ = ৬৫৫৩৬ ঠিকানা; প্রতিটি ঠিকানায় এক বাইট ধরলে ৬৪ KiB. ৩২ bit হলে ২৩২ বাইট = ৪ GiB. লক্ষ করুন, এই আকার RAM কতটুকু আছে তার ওপর নির্ভর করে না — logical আর physical address space আলাদা জিনিস, একটি অন্যটির চেয়ে বড় বা ছোট হতে পারে (ভাবনার প্রশ্ন ৫)। আর logical জগৎকে টুকরো করে RAM-এর যেকোনো জায়গায় ছড়িয়ে রাখার কৌশল হলো paging — পর্ব ৩২.

৫ · Compile → link → load — object file, symbol আর relocation

এতক্ষণ ধরে নিয়েছি প্রোগ্রাম একটি টুকরো। বাস্তবে একটি প্রোগ্রাম অনেক ফাইল থেকে আসে — আপনার কয়েকটি .c ফাইল, আর C library-র হাজারো ফাংশন। প্রতিটি .c ফাইল (তার #include করা সবকিছুসহ) আলাদা করে compile হয়; এই এককের নাম translation unit. Preprocessor, compiler আর assembler-এর ভেতরের কাজ C কোর্সের toolchain পর্বে দেখানো আছে; এখানে আমরা দেখব OS-এর দিক — ঠিকানা কোথায় ফাঁকা থাকে, আর কে কখন তা পূরণ করে।

  1. Compile (gcc -c) — প্রতিটি translation unit থেকে একটি object file (.o, Windows-এ .obj). এতে machine code আছে, কিন্তু অন্য ফাইলের ফাংশন বা ভ্যারিয়েবলের ঠিকানার জায়গায় শূন্য বসানো। সাথে দুটি তালিকা: symbol table (এই ফাইলে কোন নাম সংজ্ঞায়িত, কোনটি বাইরে থেকে লাগবে) আর relocation entry (কোন বাইটে কোন নামের ঠিকানা বসাতে হবে)।
  2. Link (ld, সাধারণত gcc-ই ডাকে) — linker সব object file আর library একসাথে নেয়। প্রথমে symbol resolution: প্রতিটি undefined নামের জন্য ঠিক একটি সংজ্ঞা খোঁজা। তারপর section সাজিয়ে প্রত্যেকের ঠিকানা ঠিক করা, আর relocation: ফাঁকা জায়গায় সেই ঠিকানা বসানো। ফল একটি executable, Linux-এ ELF ফরম্যাটে।
  3. Load (execve) — চালানোর সময় loader ফাইলটি পড়ে প্রসেসের address space-এ বসায়, দরকারি shared library আনে আর main-এর আগে শুরুর কোডে লাফ দেয়।

নিচের bash স্ক্রিপ্ট দুটি translation unit বানায়: util.c একটি ভ্যারিয়েবল আর একটি ফাংশন সংজ্ঞায়িত করে, main.c শুধু সেগুলো ব্যবহার করে। তারপর nm দিয়ে symbol, readelf দিয়ে relocation entry দেখায়, link করে চালায়, আর link-এর পরের symbol দেখায়:

# two translation units: util.c defines, main.c only uses
cat > util.c <<'EOF'
int counter = 5;                       /* defined here (data) */
int square(int x) { return x * x; }    /* defined here (text) */
EOF
cat > main.c <<'EOF'
#include <stdio.h>
extern int counter;                    /* lives in another file */
int square(int x);
int main(void) { printf("%d\n", square(counter)); return 0; }
EOF
gcc -c util.c main.c                   # compile only -> util.o, main.o
echo "== nm util.o ==";  nm util.o
echo "== nm main.o (U = undefined, needs someone else) =="; nm main.o
echo "== relocation entries in main.o (offset, type, symbol) =="
readelf -rW main.o | sed -n '/rela.text/,/^$/p' | awk 'NR>2 && NF {print $1, $3, $5}'
echo "== link and run =="
gcc -o app main.o util.o && ./app
echo "== nm app: now with addresses =="
nm app | grep -E ' (square|counter|main)$|printf'

একটি আসল রানের আউটপুট:

== nm util.o ==
0000000000000000 D counter
0000000000000000 T square
== nm main.o (U = undefined, needs someone else) ==
                 U counter
0000000000000000 T main
                 U printf
                 U square
== relocation entries in main.o (offset, type, symbol) ==
000000000000000a R_X86_64_PC32 counter
0000000000000011 R_X86_64_PLT32 square
000000000000001a R_X86_64_PC32 .rodata
0000000000000027 R_X86_64_PLT32 printf
== link and run ==
25
== nm app: now with addresses ==
0000000000004010 D counter
0000000000001149 T main
                 U printf@GLIBC_2.2.5
000000000000117b T square

লাইন ধরে পড়ুন। util.o-তে counter হলো D (data-তে সংজ্ঞায়িত), square হলো T (text-এ)। দুজনের ঠিকানাই 0 — কারণ প্রতিটি object file নিজের section-এর শুরু থেকে গোনে; আসল ঠিকানা এখনো কেউ জানে না। main.o-তে শুধু main সংজ্ঞায়িত; counter, square আর printf তিনটিই U — “অন্য কেউ দেবে”। Relocation-এর চারটি নোট বলছে main-এর কোডের কোন বাইটে কী বসাতে হবে: offset 0xa-তে counter-এর ঠিকানা (PC-আপেক্ষিক, R_X86_64_PC32), 0x11-এ square-এর কল, 0x1a-তে "%d\n" লেখাটির ঠিকানা (.rodata), আর 0x27-এ printf-এর কল। PLT32 ধরনটি বলছে “দরকার হলে PLT-র মাধ্যমে ডাকতে হবে” — অংশ ৬-এ এর মানে বুঝবেন। Link-এর পর ফল 25 (৫ × ৫), আর nm app-এ counter, main, square সবাই নিজের ঠিকানা পেয়েছে (0x4010, 0x1149, 0x117b). কিন্তু printf এখনো U! Linker তাকে আপনার ফাইলে কপি করেনি; শুধু লিখে রেখেছে “glibc-র 2.2.5 সংস্করণের printf লাগবে”। সেটি আসবে চালানোর সময়। আরেকটি বিষয়: 0x1149-এর মতো ছোট ঠিকানাগুলোও চূড়ান্ত নয় — এই executable PIE (অংশ ৭), তাই এগুলো প্রোগ্রামের শুরুর বিন্দু থেকে দূরত্ব, আর শুরুর বিন্দু প্রতিবার এলোমেলো। ঠিক কোন offset বা ঠিকানা আসবে, তা compiler-এর সংস্করণভেদে বদলাতে পারে; গঠন একই থাকে।

Symbol resolution-এর দুটি ব্যর্থতা প্রতিটি C প্রোগ্রামারের চেনা। কোনো নামের সংজ্ঞা একেবারেই না পেলে: undefined reference to `square' (যেমন util.o link করতে ভুলে গেলে)। দুটি ফাইলে একই global সংজ্ঞা থাকলে: multiple definition of `counter'. দুটোই linker-এর error, compiler-এর নয় — তাই প্রতিটি ফাইল আলাদা করে ঠিকঠাক compile হলেও শেষে আটকে যেতে পারে।

Static linking বনাম dynamic linking

উপরে printf কেন executable-এ ঢুকল না? কারণ ডিফল্টভাবে gcc dynamic linking করে। দুটি পথ পাশাপাশি দেখুন:

📦 Static linking

Linker library-র (libc.a-র মতো archive) দরকারি object file executable-এর ভেতরে কপি করে দেয়। চালাতে আর কিছু লাগে না, যেকোনো একই ধরনের মেশিনে চলে। দাম: ফাইল বড়, প্রতিটি প্রোগ্রামে library-র আলাদা কপি, আর library-তে নিরাপত্তা-ত্রুটি সারাতে প্রতিটি প্রোগ্রাম আবার link করতে হয়।

gcc -static

🔗 Dynamic linking

Executable-এ শুধু লেখা থাকে কোন shared library লাগবে (Linux-এ .so, Windows-এ DLL, macOS-এ .dylib). চালু হওয়ার সময় dynamic linker সেটি আনে। RAM-এ library-র কোডের একটিই কপি সব প্রসেস ভাগ করে; library হালনাগাদ করলে সব প্রোগ্রাম নতুনটি পায়।

ডিফল্ট

এবার একটি printf কল source থেকে CPU পর্যন্ত কোন পথে যায়, ধাপে ধাপে দেখুন। প্রতিটি ধাপে ফাইলের ভেতরের ঠিকানাটি কীভাবে বদলায় খেয়াল করুন:

  1. Sourcehi.cনাম: printf
  2. Compilehi.oফাঁকা + নোট
  3. LinkhiPLT, GOT, NEEDED
  4. execvekernelফাইল map করা
  5. ld.solibc.so.6library আনা
  6. Run42কল পৌঁছাল
hi.cশুধু একটি নাম
#include <stdio.h>
int main(void) {
    printf("%d\n", 42);
    return 0;
}
hi.ocall-এর লক্ষ্য ফাঁকা
main:  ...
       e8 00 00 00 00      call ????
relocation: offset of those 4 bytes, R_X86_64_PLT32, printf - 4
nm: U printf
hi (executable)কল যায় PLT-তে
main:        call printf@plt
printf@plt:  jmp  *GOT[printf]     ; GOT slot still to be filled
dynamic section: NEEDED libc.so.6
interpreter:     /lib64/ld-linux-x86-64.so.2
kernel: execve("./hi")মানচিত্র তৈরি
map hi            at a random base      (PIE + ASLR)
map ld-linux.so.2 at another random base
new stack: argv, envp, auxv
jump to ld.so, not to main
ld.solibrary আর ঠিকানা
read NEEDED -> find libc.so.6 -> mmap it (random base)
look up "printf" in libc's symbol table
GOT[printf] = 0x7f...  (address inside libc)   ; BIND_NOW
jump to the program's entry -> main
CPUপ্রতিটি কলে
main -> printf@plt -> jmp *GOT[printf] -> printf in libc
printf -> write(1, "42\n", 3) -> system call
$ ./hi
42
  1. Source — শুধু একটি নাম। আপনার কোডে printf একটি নাম মাত্র। stdio.h শুধু ঘোষণা দেয় যে এমন ফাংশন আছে; তার কোড এখানে নেই।
  2. Compile — ফাঁকা জায়গা আর একটি নোট। Compiler call নির্দেশ (বাইট e8) লিখল, কিন্তু লক্ষ্যের চার বাইট শূন্য। সাথে relocation entry: “এই চার বাইটে printf-এর আপেক্ষিক ঠিকানা বসাতে হবে”। Symbol table-এ printf হলো U.
  3. Link — কল এখন PLT-তে। Linker দেখল printf আসবে libc.so.6 থেকে, তাই call-এর লক্ষ্য বানাল নিজের তৈরি ছোট একটি stub, printf@plt, যেটি GOT-এর একটি ঘরে লেখা ঠিকানায় লাফ দেয়। ফাইলে লিখল “libc.so.6 লাগবে” (NEEDED) আর কোন dynamic linker দিয়ে চালাতে হবে (interpreter). Call আর PLT-র মধ্যের দূরত্ব এখন পাকা — দুটি একই ফাইলে।
  4. execve — kernel মানচিত্র বানায়। আপনি ./hi চালালেন। Kernel ELF header পড়ে প্রোগ্রামের অংশগুলো নতুন address space-এ map করল, একটি এলোমেলো শুরুর ঠিকানায়। Interpreter হিসেবে লেখা ld-linux-x86-64.so.2-কেও map করল, আর প্রথমে লাফ দিল তার কাছে — main-এ নয়।
  5. ld.so — library আনা আর GOT পূরণ। Dynamic linker NEEDED তালিকা পড়ে libc.so.6 খুঁজে mmap করল (আরেকটি এলোমেলো ঠিকানায়), printf-এর আসল ঠিকানা হিসাব করে GOT-এর ঘরে লিখল। এটি load-time binding-এর আধুনিক রূপ — তবে কোডে নয়, শুধু ডেটার একটি টেবিলে। Ubuntu 24.04-এর মতো সিস্টেমে কাজটি শুরুতেই হয় (BIND_NOW); lazy binding-এ প্রথম কলের সময় হয় (পরের ডায়াগ্রাম)।
  6. Run — কল পৌঁছাল। এখন প্রতিটি কলে main → printf@plt → GOT থেকে ঠিকানা → libc-র printf. আর প্রতিটি ঠিকানা-ব্যবহার MMU logical থেকে physical-এ অনুবাদ করছে (execution-time binding). Printf শেষে write system call দিয়ে “42” ছাপল। অর্থাৎ একটি কলের পেছনে তিন রকম binding-ই কাজ করেছে: link time-এ PLT পর্যন্ত, load time-এ GOT, আর execution time-এ physical ঠিকানা।

৬ · Dynamic linking-এর ভেতরে — PLT, GOT, lazy binding আর dlopen

প্রশ্ন জাগে: library-র কোডের ঠিকানা প্রতিবার আলাদা হলে কলের ঠিকানা কোডে সরাসরি লিখে দিলেই তো হয়? সমস্যা হলো, তাতে কোড পাতাগুলো প্রতিটি প্রসেসে আলাদা করে বদলাতে হতো — তখন সেগুলো আর ভাগ করা যেত না, আর লেখা যায় এমন কোড নিরাপত্তার জন্যও খারাপ। সমাধান হলো এক স্তর পরোক্ষতা (indirection): কোড কখনো বদলায় না; বদলায় শুধু ডেটার একটি টেবিল.

  • GOT (global offset table) — প্রতিটি প্রসেসের ডেটা অংশে একটি টেবিল, যার প্রতিটি ঘরে একটি বাইরের ফাংশন বা ভ্যারিয়েবলের আসল ঠিকানা লেখা হয়। প্রতি প্রসেসে আলাদা কপি।
  • PLT (procedure linkage table) — কোড অংশে ছোট ছোট stub, প্রতিটি বাইরের ফাংশনের জন্য একটি। Stub-এর কাজ: “GOT-এর আমার ঘরে যে ঠিকানা আছে, সেখানে লাফ দেওয়া”। কোড শুধু-পড়ার আর সব প্রসেসে ভাগ করা।

এখন ঠিকানা কখন খোঁজা হবে? দুটি নীতি। Lazy binding: শুরুতে GOT-এর ঘরে একটি “আগে আসল ঠিকানা খুঁজে আনতে হবে” ধরনের ঠিকানা থাকে, আর প্রথম কলে dynamic linker আসল ঠিকানা খুঁজে লিখে দেয়। যে ফাংশন কখনো ডাকা হয় না, তার খোঁজার খরচও লাগে না — বড় প্রোগ্রামের শুরু দ্রুত হয়। BIND_NOW: শুরুতেই সব খোঁজা, তারপর RELRO-র মাধ্যমে GOT শুধু-পড়ার করে দেওয়া — একটু ধীর শুরু, কিন্তু বাগ দিয়ে GOT বদলে কলকে অন্য দিকে ঘোরানো যায় না। নিচে lazy পথটি সরলীকৃতভাবে দেখুন (ঠিকানাগুলো সরলীকৃত উদাহরণ):

1main:        call printf@plt2printf@plt:  jmp  *GOT[printf]      ; slot at 0x3fd03             push 0 ; jmp PLT0       ; lazy stub4PLT0:        push GOT[1] ; jmp *GOT[2]5ld.so:       _dl_runtime_resolve -> find "printf"6             GOT[printf] = libc printf; jump there7libc:        printf(...) ... ret8main (later): call printf@plt
প্রতিটি ধাপের পর
GOT[printf]-এ
কে চলছে
resolver কতবার
  1. প্রথম কল। main ডাকল printf@plt. Main জানেই না printf কোথায়; সে শুধু জানে নিজের ফাইলের PLT stub-এর আপেক্ষিক ঠিকানা, যা link time-এ পাকা হয়েছিল।
  2. GOT থেকে ঠিকানা পড়া। Stub GOT-এর ঘর (0x3fd0) থেকে ঠিকানা পড়ে লাফ দিল। কিন্তু lazy binding-এ এই ঘরে এখনো আসল printf নেই — আছে PLT-রই পরের নির্দেশের ঠিকানা (0x1036). অর্থাৎ লাফটি ফিরে এল stub-এর বাকি অংশে।
  3. “আমি printf, আমার নম্বর ০”। Stub-এর বাকি অংশ একটি সংখ্যা stack-এ রাখে — relocation তালিকায় printf-এর ক্রমিক নম্বর — যাতে পরে বোঝা যায় কোন ফাংশনের ঠিকানা লাগবে। তারপর সাধারণ PLT0-এ লাফ।
  4. PLT0 — সবার সাধারণ দরজা। PLT0 GOT[1]-এ রাখা “এটি কোন library বা প্রোগ্রাম” তথ্য stack-এ রাখে আর GOT[2]-এ লেখা ঠিকানায় লাফ দেয়। GOT[1] আর GOT[2] dynamic linker শুরুতেই পূরণ করে রেখেছিল — GOT[2] হলো resolver ফাংশন।
  5. Resolver খোঁজে। Dynamic linker-এর _dl_runtime_resolve ক্রমিক নম্বর থেকে নাম “printf” বের করল, লোড হওয়া library-গুলোর symbol table-এ (hash table দিয়ে দ্রুত) খুঁজে libc-তে পেল। এই খোঁজাটিই lazy binding-এর আসল খরচ — প্রতিটি ফাংশনে মাত্র একবার।
  6. GOT-এ লিখে দেওয়া। Resolver আসল ঠিকানা GOT[printf]-এ লিখল, তারপর সরাসরি সেখানে লাফ দিল। এখন থেকে GOT-এর এই ঘর স্থায়ীভাবে libc-র printf দেখায়। (কোড একটুও বদলায়নি — বদলেছে শুধু ডেটা।)
  7. Printf চলে। libc-র printf কাজ শেষ করে ফিরে যায় main-এ — যেন main তাকে সরাসরি ডেকেছিল। Stack-এ রাখা বাড়তি তথ্য resolver আগেই সরিয়ে নিয়েছে।
  8. দ্বিতীয় কল। পরে main আবার printf ডাকল। একই stub, একই jmp *GOT[printf] — কিন্তু এবার ঘরে আসল ঠিকানা।
  9. সরাসরি libc-তে। একটি বাড়তি লাফ ছাড়া আর কোনো খরচ নেই; resolver-এর গণনা এখনো ১. BIND_NOW-এ ধাপ ২–৬ চালু হওয়ার সময়ই হয়ে যায়, তাই প্রথম কল থেকেই এই পথ। নোট: আধুনিক x86-64-এ CET/IBT-এর জন্য stub-এর শুরুতে endbr64 থাকে আর PLT দুই ভাগে ভাগ করা — এখানে তা বাদ দেওয়া হয়েছে।

এবার নিজের চোখে দেখুন। নিচের স্ক্রিপ্ট একই প্রোগ্রাম তিনভাবে বানায় — ডিফল্ট dynamic, lazy binding চেয়ে dynamic, আর static — তারপর আকার, dependency, ফাইলের ধরন, তিনটি আলাদা প্রসেসে libc-এর শুরুর ঠিকানা, আর glibc-এর LD_DEBUG দিয়ে printf কখন bind হয় তা দেখায়। (ldd কমান্ডটি শুধু নিজের বা বিশ্বস্ত ফাইলে চালাবেন; অচেনা ফাইলের জন্য readelf -d নিরাপদ।)

# one tiny program, linked three ways
cat > hi.c <<'EOF'
#include <stdio.h>
#include <unistd.h>
int main(void) { write(2, "main starts\n", 12); printf("%d\n", 42); return 0; }
EOF
gcc -o hi_dyn hi.c                     # default: dynamic linking
gcc -Wl,-z,lazy -o hi_lazy hi.c        # dynamic, ask for lazy binding
gcc -static -o hi_sta hi.c 2>/dev/null || echo "(static libc not installed here)"
echo "== file sizes (bytes) =="
ls -l hi_dyn hi_sta 2>/dev/null | awk '{print $5, $9}'
echo "== what hi_dyn needs at load time =="
readelf -d hi_dyn | grep -E 'NEEDED|FLAGS' | awk '{$1=""; print}'
ldd ./hi_dyn | awk '{print $1}'
echo "== hi_sta needs =="; ldd ./hi_sta 2>&1 | tr -d '\t'
echo "== type of hi_dyn =="; readelf -h hi_dyn | grep 'Type:' | sed 's/^ *//'
echo "== libc start address in three separate processes =="
cat /proc/sys/kernel/randomize_va_space
for i in 1 2 3; do grep -m1 'libc.so' /proc/self/maps | cut -d- -f1; done
for p in hi_dyn hi_lazy; do
  echo "== $p: when is printf bound? =="
  LD_DEBUG=bindings ./$p 2>&1 >/dev/null | grep -oE "main starts|symbol .printf'"
done

একটি আসল রানের আউটপুট (ঠিকানাগুলো প্রতিবার আলাদা হবে):

== file sizes (bytes) ==
16000 hi_dyn
785304 hi_sta
== what hi_dyn needs at load time ==
 (NEEDED) Shared library: [libc.so.6]
 (FLAGS) BIND_NOW
 (FLAGS_1) Flags: NOW PIE
linux-vdso.so.1
libc.so.6
/lib64/ld-linux-x86-64.so.2
== hi_sta needs ==
not a dynamic executable
== type of hi_dyn ==
Type:                              DYN (Position-Independent Executable file)
== libc start address in three separate processes ==
2
73f782cdd000
76b2d7b06000
734a72cb8000
== hi_dyn: when is printf bound? ==
symbol `printf'
main starts
== hi_lazy: when is printf bound? ==
main starts
symbol `printf'

আকার: একই দুই লাইনের প্রোগ্রাম dynamic-এ ১৬০০০ বাইট, static-এ ৭৮৫৩০৪ বাইট — প্রায় ৪৯ গুণ, কারণ printf আর তার পেছনের সব libc কোড ফাইলে কপি হয়েছে। Dependency: hi_dyn-এর NEEDED শুধু libc.so.6; ldd আরও দুটি দেখায় — dynamic linker নিজে আর kernel-এর দেওয়া vDSO (পর্ব ৫)। Static ফাইলের কোনো dependency নেই। FLAGS-এ BIND_NOW: এই runner-এর Ubuntu gcc ডিফল্টভাবেই শুরুতে সব bind করে — শেষ অংশে তার প্রমাণ: hi_dyn-এ printf-এর binding “main starts”-এর আগে, আর -z lazy দিয়ে বানানো hi_lazy-তে পরে, প্রথম কলের মুহূর্তে। ASLR: তিনটি আলাদা grep প্রসেসে libc তিনটি আলাদা ঠিকানায় (মান 2 মানে পূর্ণ ASLR চালু)। একই libc.so.6 ফাইল, অথচ প্রতিবার অন্য জায়গায় — সম্ভব হয়েছে কারণ এর কোড position-independent (পরের অংশ)।

Dynamic loading — দরকার হলে তবেই আনা

Dynamic linking-এ কোন library লাগবে তা ফাইলে আগে থেকে লেখা থাকে। Dynamic loading আরও এক ধাপ নমনীয়: প্রোগ্রাম চলার মাঝে নিজেই ঠিক করে কোন library আনবে। POSIX-এ dlopen library লোড করে একটি handle দেয়, dlsym তার ভেতরের নাম থেকে ঠিকানা দেয়; Windows-এ একই কাজ LoadLibrary আর GetProcAddress. Plugin ব্যবস্থা এভাবেই চলে. পাঠ্যবইয়ের ভাষায় dynamic loading-এর সুবিধা: যে রুটিন কখনো লাগে না (যেমন বিরল ত্রুটি সামলানোর কোড), সেটি কখনো মেমরিতে আসে না, আর এর জন্য OS-এর বিশেষ সমর্থন লাগে না — প্রোগ্রামের নকশাই যথেষ্ট।

নিচের C প্রোগ্রাম /proc/self/maps পড়ে (পর্ব ১১) দেখায় কোন ঠিকানা কোন mapping-এ পড়ে, আর libm.so.6 (math library) dlopen-এর আগে আর পরে মেমরিতে আছে কি না:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <dlfcn.h>
int global = 1;
/* scan /proc/self/maps: count lines naming lib; name the mapping that holds p */
static int scan(const char *lib, const void *p, char *who, size_t n) {
    FILE *f = fopen("/proc/self/maps", "r"); char line[512], perm[8], path[256];
    unsigned long lo, hi, a = (unsigned long)(uintptr_t)p; int hits = 0;
    while (f && fgets(line, sizeof line, f)) {
        path[0] = '\0';
        if (sscanf(line, "%lx-%lx %7s %*s %*s %*s %255s", &lo, &hi, perm, path) < 3) continue;
        if (lib && strstr(path, lib)) hits++;
        if (p && a >= lo && a < hi) snprintf(who, n, "%s [%s]",
            strrchr(path, '/') ? strrchr(path, '/') + 1 : path, perm);
    }
    if (f) fclose(f);
    return hits;
}
int main(void) {
    char w[300] = "?";
    printf("libm mappings before dlopen: %d\n", scan("libm.so", NULL, w, sizeof w));
    void *h = dlopen("libm.so.6", RTLD_NOW);          /* dynamic loading */
    if (!h) { printf("dlopen failed: %s\n", dlerror()); return 1; }
    double (*cosp)(double);
    *(void **)&cosp = dlsym(h, "cos");                 /* POSIX-style cast */
    printf("libm mappings after dlopen:  %d\n", scan("libm.so", NULL, w, sizeof w));
    scan(NULL, (void *)main, w, sizeof w);    printf("main   -> %s\n", w);
    scan(NULL, &global, w, sizeof w);         printf("global -> %s\n", w);
    scan(NULL, stdout, w, sizeof w);          printf("stdout -> %s\n", w);
    scan(NULL, *(void **)&cosp, w, sizeof w); printf("cos    -> %s\n", w);
    printf("cos(0) = %.1f, main below 4 GiB: %s\n", cosp(0.0),
           (uintptr_t)main < 0x100000000u ? "yes" : "no");
    return dlclose(h);
}

আসল আউটপুট:

libm mappings before dlopen: 0
libm mappings after dlopen:  5
main   -> prog.exe [r-xp]
global -> prog.exe [rw-p]
stdout -> libc.so.6 [rw-p]
cos    -> libm.so.6 [r-xp]
cos(0) = 1.0, main below 4 GiB: yes

প্রথম দুই লাইন dynamic loading-এর প্রমাণ: dlopen-এর আগে প্রসেসের মানচিত্রে libm-এর কোনো লাইন নেই, পরে ৫টি — একটি library ফাইল কয়েকটি mapping-এ বসে, কারণ কোড (r-x), শুধু-পড়ার ডেটা আর লেখা যায় এমন ডেটার অনুমতি আলাদা। পরের চার লাইন দেখায় একই address space-এ আলাদা আলাদা “বাসিন্দা”: main প্রোগ্রামের নিজের ফাইলের (prog.exe, এই runner-এ executable-এর নাম) কোড অংশে, global তার লেখা যায় এমন ডেটায়, stdout-এর FILE অবজেক্ট libc-র ডেটায়, আর cos সদ্য আনা libm-এর কোডে। শেষ লাইনটি চমকপ্রদ: এই C runner-এর compiler PIE বানায় না, তাই main বসেছে নিচের দিকের একটি নির্দিষ্ট ঠিকানায় (৪ GiB-এর নিচে, প্রচলিত non-PIE ঠিকানা 0x400000-এর কাছে) — প্রতিবার একই। অথচ libc আর libm প্রতিবার এলোমেলো জায়গায়। অর্থাৎ একই প্রসেসে প্রোগ্রামের ঠিকানা link time-এ পাকা, library-গুলোর load time-এ। আগের bash runner-এর gcc ডিফল্টভাবে PIE বানায় — সেখানে main-ও সরে যেত। *(void **)&cosp = dlsym(...) লাইনটি POSIX-এর সুপারিশ করা লেখার ধরন, কারণ মানদণ্ড C-তে void * থেকে ফাংশন পয়েন্টারে সরাসরি রূপান্তর নিশ্চিত করে না।

$ readelf -d /usr/bin/ls | grep NEEDED       # what it needs
 0x0000000000000001 (NEEDED)  Shared library: [libselinux.so.1]
 0x0000000000000001 (NEEDED)  Shared library: [libc.so.6]
$ ldconfig -p | grep 'libm.so.6'               # where the loader looks
	libm.so.6 (libc6,x86-64) => /lib/x86_64-linux-gnu/libm.so.6
PS> dumpbin /dependents C:\Windows\System32\notepad.exe
  Image has the following dependencies:
    KERNEL32.dll
    GDI32.dll
    USER32.dll
    ...
$ otool -L /bin/ls
/bin/ls:
	/usr/lib/libutil.dylib (compatibility version 1.0.0, ...)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, ...)

তিন OS-এর ধারণা এক, খুঁটিনাটি আলাদা। Windows-এ PLT/GOT-এর জায়গায় আছে import address table (IAT) — loader সাধারণত প্রোগ্রাম চালু হওয়ার সময়ই DLL-এর ফাংশনের ঠিকানা এতে লিখে দেয়, আর “delay-load” নামে lazy-র মতো একটি ঐচ্ছিক ব্যবস্থাও আছে। macOS-এ dynamic linker-এর নাম dyld, ফরম্যাট Mach-O. উপরের আউটপুটগুলো উদাহরণ — সংস্করণভেদে তালিকা বদলায়।

৭ · Position-independent code — আর ASLR কেন এর ওপর নির্ভর করে

একটি shared library দুইশো প্রসেসে ভাগ হচ্ছে, আর ASLR-এর কারণে প্রতিটি প্রসেসে সেটি বসছে আলাদা logical ঠিকানায়। তবু RAM-এ তার কোডের একটিই কপি। এটি কীভাবে সম্ভব? কোডের ভেতরে যদি কোথাও লেখা থাকত “ঠিকানা 0x7f3a1c060100-এ লাফ দিতে হবে”, তাহলে প্রতিটি প্রসেসের জন্য সেটি আলাদা হতে হতো, আর কোড ভাগ করা যেত না। সমাধান position-independent code (PIC): এমন machine code, যাতে কোনো পরম (absolute) ঠিকানা লেখা থাকে না।

  • নিজের ভেতরের জিনিস — নিজের ফাংশন বা ডেটা — খোঁজা হয় PC-আপেক্ষিক হিসাবে: “এই নির্দেশ থেকে ৩০০০ বাইট দূরে”। পুরো library যেখানেই বসুক, ভেতরের দূরত্ব একই। x86-64-এ এজন্য RIP-relative addressing আছে, যা উপরের relocation তালিকার R_X86_64_PC32-এ দেখেছেন। (32-bit x86-এ এমন সরাসরি ব্যবস্থা ছিল না, তাই PIC সেখানে কিছুটা ধীর ছিল।)
  • বাইরের জিনিস — অন্য library-র ফাংশন বা ভ্যারিয়েবল — খোঁজা হয় GOT থেকে, যার নিজের অবস্থানও PC-আপেক্ষিক। GOT প্রতি প্রসেসে আলাদা, কোড সবার এক।

Shared library তৈরি হয় gcc -fPIC -shared দিয়ে। আর যে executable নিজেও PIC দিয়ে তৈরি, তার নাম PIE (position-independent executable) — -fPIE -pie, আজকের Ubuntu, Fedora, Debian-এর gcc-তে ডিফল্ট। উপরের bash আউটপুটে Type: DYN (Position-Independent Executable file) ঠিক সেটিই বলছিল।

ASLR-এর সাথে সম্পর্ক

পর্ব ৮-এ (ASLR অংশ) দেখেছেন, ASLR প্রতিবার stack, heap, mmap এলাকা আর library-র শুরুর ঠিকানা এলোমেলো করে, যাতে কোনো মেমরি-বাগ কাজে লাগাতে আক্রমণকারী দরকারি কোডের ঠিকানা আগে থেকে না জানে। কিন্তু কোড যদি একটি নির্দিষ্ট ঠিকানা ধরে লেখা হয় (non-PIE), তাকে সরাতে হলে দুটি পথ: হয় load time-এ কোডের সব ঠিকানা ঠিক করা (“text relocation” — কোড পাতা লেখা যায় এমন করতে হয়, ভাগ করা যায় না, ধীরও), নয়তো না সরানো। বাস্তবে non-PIE executable না সরিয়েই চলে — ঠিক যেমন উপরের C runner-এ main প্রতিবার একই নিচু ঠিকানায় ছিল। তাই ASLR-এর পূর্ণ সুরক্ষা পেতে প্রোগ্রামকে PIE হতে হয়; নইলে প্রোগ্রামের নিজের কোড আর ডেটা অনুমেয় জায়গায় থেকে যায়।

একটি বাড়ির ঠিকানা দেওয়ার দুই ধরন ভাবুন। “বাড়ি নম্বর ১২, রোড ৫, ধানমন্ডি” — পরম ঠিকানা; বাড়িটি অন্য এলাকায় সরালে সব চিঠির ঠিকানা বদলাতে হবে। আর “মসজিদের পেছনের গলিতে, তিন বাড়ি পরে” — আপেক্ষিক ঠিকানা; মসজিদ আর গলিসহ পুরো পাড়া অন্য জায়গায় সরালেও নির্দেশনা একই কাজ করে। PIC কোড দ্বিতীয় ধরনে লেখা। আর পাড়ার বাইরের কারও ঠিকানা লাগলে মোড়ের দোকানে রাখা একটি খাতা (GOT) দেখতে হয়; পাড়া সরলে শুধু খাতাটি হালনাগাদ হয়।

আরও গভীরে: Linux-এ execve-এর ELF লোডার (fs/binfmt_elf.c-এর load_elf_binary) program header-এর PT_INTERP দেখে dynamic linker-কে map করে, আর নতুন stack-এ “auxiliary vector” রাখে — AT_BASE (ld.so কোথায়), AT_ENTRY (প্রোগ্রামের entry), AT_RANDOM (১৬টি এলোমেলো বাইট) ইত্যাদি; LD_SHOW_AUXV=1 /bin/true চালালে এগুলো দেখা যায়। Dynamic linker নিজেও একটি shared object, তাই কাজ শুরুর আগে তাকে নিজের relocation নিজে করতে হয় (“bootstrap relocation”). Windows ASLR অন্যভাবে আপস করে: একটি DLL-এর এলোমেলো ঠিকানা সাধারণত প্রতি boot-এ একবার বাছাই হয় আর সব প্রসেসে একই থাকে, যাতে relocate করা কোড পাতাও ভাগ করা যায় — তাই Windows-এ একই DLL দুটি প্রসেসে সাধারণত একই ঠিকানায় দেখা যায়, যেখানে Linux-এ প্রতিটি প্রসেসে আলাদা।

৮ · Swapping — পুরো প্রসেস ডিস্কে, আর overlay-র ইতিহাস

সবশেষে সেই প্রশ্ন: সব প্রসেস RAM-এ না আঁটলে? ক্লাসিক উত্তর standard swapping (standard swapping): কোনো একটি প্রসেস — সাধারণত যে অনেকক্ষণ অপেক্ষায় বা কম গুরুত্বপূর্ণ — তার পুরো মেমরি-চিত্র একটি দ্রুত ডিস্কে লিখে দেওয়া (swap out), আর জায়গা খালি হলে আবার পুরোটা ফিরিয়ে আনা (swap in). যে ডিস্ক-এলাকায় এই চিত্র রাখা হয়, তার নাম backing store. পর্ব ৮-এর suspended অবস্থা আর medium-term scheduler ঠিক এই কাজের নাম।

Execution-time binding থাকলে প্রসেস ফিরে এসে RAM-এর যেকোনো খালি জায়গায় বসতে পারে — শুধু PCB-র relocation মান বদলালেই হয়, যা সিমুলেটরে দেখেছেন। Compile-time বা load-time binding-এ তাকে ঠিক আগের জায়গাতেই ফিরতে হতো।

পরীক্ষার হিসাব — swap-এর খরচ

Swap-এর প্রধান খরচ হলো ডেটা সরানোর সময় (transfer time), আর সেটি প্রসেসের আকারের সমানুপাতিক:

swap time (এক দিকে) = প্রসেসের আকার ÷ transfer rate (+ ডিস্কের latency, দেওয়া থাকলে)
context switch-এ মোট = swap out + swap in
প্রশ্নহিসাবউত্তর
ক) ১০০ MB প্রসেস, transfer rate ৫০ MB/sএক দিকে ১০০ ÷ ৫০ = ২ সেকেন্ড; out + in = ২ + ২৪ সেকেন্ড (৪০০০ ms)
খ) ১০ MB প্রসেস, ৪০ MB/s, গড় latency ৮ ms১০ ÷ ৪০ = ০.২৫ s = ২৫০ ms; ২৫০ + ৮ = ২৫৮ ms এক দিকে; × ২৫১৬ ms
গ) (খ)-এর সিস্টেমে round robin; swap-এর তুলনায় quantum কেমন হবে?প্রতিটি switch-এ swap লাগলে ৫১৬ ms-এর তুলনায় quantum অনেক বড় না হলে CPU-র সময়ের বেশিরভাগ যাবে swap-এQuantum ≫ ৫১৬ ms
ঘ) ১০০ MB, কিন্তু আধুনিক SSD ধরুন ~২০০০ MB/s (অনুমান)১০০ ÷ ২০০০ = ০.০৫ s = ৫০ ms এক দিকে~১০০ ms মোট

প্রশ্নে transfer rate যা দেওয়া থাকবে, সেটিই ব্যবহার করবেন — ৫০ MB/s পুরোনো hard disk-এর মতো একটি পাঠ্যবই-সংখ্যা; আসল গতি ডিস্ক, interface আর ফাইলের অবস্থাভেদে অনেক বদলায়। তিনটি সাধারণ ফাঁদ: (১) out আর in দুবার গোনা ভুলে যাওয়া; (২) MB (মেগাবাইট) আর Mb (মেগাবিট) গুলিয়ে ফেলা — ৮ গুণ পার্থক্য; (৩) একক মেলানো — সেকেন্ড থেকে ms করতে ১০০০ দিয়ে গুণ। আরেকটি বাস্তব কথা: শুধু যতটুকু মেমরি আসলে ব্যবহার হচ্ছে, ততটুকুই swap করা দরকার — তাই প্রসেস কতটুকু মেমরি ব্যবহার করছে তা OS-কে জানাতে হয়।

অসমাপ্ত I/O-র ফাঁদ: একটি প্রসেস ডিস্ক থেকে নিজের buffer-এ ডেটা পড়ার অপেক্ষায় আছে, আর ঠিক তখন তাকে swap out করে সেই জায়গায় অন্য প্রসেস বসানো হলো। ডিভাইস (DMA দিয়ে) ডেটা লিখে দেবে নতুন প্রসেসের মেমরিতে! সমাধান দুটি: অসমাপ্ত I/O থাকা প্রসেসকে swap না করা, অথবা I/O সবসময় kernel-এর নিজের buffer-এ করে পরে কপি করা (double buffering).

আজকের বাস্তবতা — পুরো প্রসেস নয়, page

পুরো প্রসেস সরানো এত ধীর যে আধুনিক Linux, Windows বা macOS সাধারণ অবস্থায় এটি করে না। তারা swap করে page ধরে — মেমরির ছোট টুকরো (সাধারণত ৪ KiB), শুধু যেগুলো কম ব্যবহৃত (পর্ব ৩৫–৩৭)। Linux-এর swap partition বা swap file, Windows-এর pagefile.sys — দুটোই এই page-ভিত্তিক swap.

মোবাইলে ক্লাসিক অর্থে swap প্রায় নেই। iOS অ্যাপের মেমরি flash-এ swap করে না (নতুন কিছু iPad-এ iPadOS 16 থেকে সীমিত swap এসেছে); RAM কমলে প্রথমে কম ব্যবহৃত পাতা চেপে ছোট করে (memory compression), অ্যাপকে মেমরি ছাড়তে সতর্ক করে, আর শেষে পেছনের অ্যাপ বন্ধ করে দেয় (Jetsam). Android সাধারণত flash-এ swap না করে RAM-এর ভেতরেই compress করা swap (zram) ব্যবহার করে, আর চাপ বাড়লে low-memory killer (lmkd) পেছনের অ্যাপ বন্ধ করে। কারণগুলো: flash-এর লেখার আয়ু সীমিত, ব্যাটারি খরচ, আর মোবাইলে ছোট storage. সেজন্যই মোবাইল অ্যাপকে পেছনে যাওয়ার সময় নিজের অবস্থা সংরক্ষণ করতে হয় — OS যেকোনো সময় তাকে বন্ধ করে পরে নতুন করে চালু করতে পারে। খুঁটিনাটি সংস্করণ আর নির্মাতাভেদে বদলায় (পর্ব ৫৪)।

Overlay — ইতিহাসের পাতা

Virtual memory আসার আগে, RAM-এর চেয়ে বড় প্রোগ্রাম চালানোর একটি কৌশল ছিল overlay. প্রোগ্রামার নিজে প্রোগ্রামকে টুকরো করতেন: একটি অংশ সবসময় মেমরিতে (মূল নিয়ন্ত্রক আর সাধারণ ডেটা), আর বাকি টুকরোগুলো পালা করে একই মেমরি-এলাকায় আসত। পাঠ্যবইয়ের উদাহরণ একটি দুই-pass assembler: pass 1-এর কোড ৭০ KB, pass 2-এর ৮০ KB, symbol table ২০ KB, সাধারণ রুটিন ৩০ KB — একসাথে ২০০ KB, অথচ মেমরি ধরুন ১৫০ KB. Pass 1 আর pass 2 কখনো একসাথে লাগে না, তাই overlay A = symbol table + সাধারণ রুটিন + pass 1 = ১২০ KB, overlay B = symbol table + সাধারণ রুটিন + pass 2 = ১৩০ KB; সাথে ছোট একটি overlay driver (ধরুন ১০ KB) মিলে সর্বোচ্চ ১৪০ KB — আঁটে। DOS যুগের কিছু compiler (যেমন Borland-এর টুল) overlay-র সুবিধা দিত। দাম ছিল বড়: পুরো প্রোগ্রামের গঠন প্রোগ্রামারকে মাথায় রাখতে হতো, আর ভুল হলে খুঁজে পাওয়া কঠিন। পরে OS-এর virtual memory (পর্ব ৩৫) এই কাজ স্বয়ংক্রিয় করে দেয়, তাই আজ overlay প্রায় শুধু ছোট embedded সিস্টেমে দেখা যায়।

৯ · AI-এর যুগে — linking, ঠিকানা আর মেমরির প্রশ্নে AI-কে কীভাবে ব্যবহার করবেন

“undefined reference” error, “error while loading shared libraries”, বা পরীক্ষার “logical address 346, relocation 14000” — এসব প্রশ্নে AI চটপট উত্তর দেয়। কিন্তু এই বিষয়ে AI-এর ভুলগুলো খুব চেনা ধরনের, আর এই পর্বের ধারণা থাকলে আপনি সেগুলো ধরতে পারবেন:

AI-এর ভুলআসল সত্যকীভাবে যাচাই করবেন
“library পাচ্ছে না? LD_LIBRARY_PATH স্থায়ীভাবে .bashrc-এ দিন” বা “.so ফাইলে chmod 777 দিন”সাধারণত ঠিক package ইনস্টল করা, বা /etc/ld.so.conf.d/-তে পথ যোগ করে ldconfig; 777 নিরাপত্তা-ঝুঁকি, সমস্যার সমাধানও নয়readelf -d দিয়ে NEEDED দেখুন, ldconfig -p-এ খুঁজুন
Windows-এ ldd চালাতে বলা, বা Linux-এ DLL-এর পরামর্শWindows-এ dumpbin /dependents, macOS-এ otool -Lপ্রম্পটে OS, distro আর সংস্করণ লিখুন
“crash ঠিক করতে ASLR বন্ধ করুন” বা “-no-pie, -z execstack দিয়ে compile করুন”এগুলো বাগ লুকায়, সুরক্ষা কমায়; আসল বাগ খুঁজতে AddressSanitizer বা debuggerসুরক্ষা বন্ধ করার পরামর্শ পেলে থামুন, কারণ জিজ্ঞেস করুন
Swap হিসাবে শুধু এক দিক গোনা, বা MB/Mb গুলিয়ে ফেলাContext switch-এ out + in; বাইট আর বিটে ৮ গুণ পার্থক্যহাতে একক মিলিয়ে নিজে হিসাব করুন
Base-limit প্রশ্নে “base + limit পর্যন্ত বৈধ”বৈধ পরিসর base থেকে base + limit − 1সীমার ঠিক ওপরের ঠিকানাটি নিয়ে পরীক্ষা করুন
“Linux সবসময় lazy binding করে”অনেক distro এখন ডিফল্টভাবে BIND_NOW আর full RELRO দেয়readelf -d prog | grep FLAGS

Link বা load-এর সমস্যায় AI-কে কাজে লাগানোর সময় পূর্ণ তথ্য দিন, আর যাচাইয়ের পথ চেয়ে নিন:

আমার পরিবেশ: Ubuntu 24.04, x86-64, gcc 13, bash. root ছাড়া সাধারণ ব্যবহারকারী। সমস্যা: ./app চালালে "error while loading shared libraries: libfoo.so.1: cannot open shared object file" আসে। নিচে readelf -d app | grep -E 'NEEDED|RUNPATH|RPATH' আর ls -l /opt/foo/lib-এর আউটপুট দিলাম। [আউটপুট এখানে] তুমি বলো: ১. dynamic linker কোন কোন জায়গায় এবং কোন ক্রমে libfoo.so.1 খোঁজে, আর আমার ক্ষেত্রে কেন পাচ্ছে না। ২. সবচেয়ে কম ঝুঁকির সমাধান কোনটি — আর LD_LIBRARY_PATH, rpath ও ldconfig-এর মধ্যে তুলনা। ৩. কোনো পরামর্শে sudo, chmod বা সিস্টেম ফোল্ডারে ফাইল কপি থাকলে আগে তার ঝুঁকি বলো। প্রতিটি দাবির পাশে লেখো সেটি কোন man page-এ (যেমন man ld.so) মিলিয়ে দেখা যায়।

AI-এর উত্তর মানার আগে — চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • Logical আর physical address-এর পার্থক্য? — CPU যা তৈরি করে তা logical; MMU অনুবাদ করে মেমরিতে যা পাঠায় তা physical. Compile ও load-time binding-এ দুটি এক, execution-time-এ আলাদা।
  • Address binding-এর তিন সময় কী কী? — Compile time (absolute code), load time (relocatable code), execution time (MMU লাগে, চলার মাঝে সরানো যায়)।
  • Base 300040, limit 120900 — 420940 ঠিকানা বৈধ? — না; বৈধ পরিসর 300040 থেকে 420939, তাই trap.
  • Static আর dynamic linking-এর পার্থক্য, আর PLT/GOT কেন লাগে? — Static-এ library কোড ফাইলে কপি; dynamic-এ চালু হওয়ার সময় যুক্ত। কোড অপরিবর্তিত রেখে প্রতি প্রসেসের ঠিকানা GOT-এ রাখতে PLT/GOT.
  • ১০০ MB প্রসেস, ৫০ MB/s — swap-এর মোট সময়? — এক দিকে ২ s, out + in মিলে ৪ s.

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

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

প্র ০১Linux distribution-গুলো dynamic linking পছন্দ করে, অথচ Go-র মতো ভাষা বা অনেক container image static binary পছন্দ করে। কোনটি ঠিক?

দুটি পক্ষই আলাদা সমস্যার সমাধান খুঁজছে। Dynamic linking-এর পক্ষে সবচেয়ে বড় যুক্তি নিরাপত্তা-হালনাগাদ. ২০১৪ সালে OpenSSL-এর “Heartbleed” ত্রুটি প্রকাশের পর distribution-গুলো একটি libssl package হালনাগাদ করে দিল, আর সেটি ব্যবহার করা সব প্রোগ্রাম — ওয়েব সার্ভার, মেইল সার্ভার, VPN — একসাথে সুরক্ষিত হলো (প্রোগ্রামগুলো আবার চালু করার পর)। প্রতিটি প্রোগ্রাম static হলে প্রত্যেকটিকে আলাদা করে আবার build করে পাঠাতে হতো। দ্বিতীয় যুক্তি মেমরি আর disk: একটি সাধারণ Linux ডেস্কটপে কয়েকশো প্রসেস একই libc ব্যবহার করে, আর RAM-এ তার কোডের একটি কপিই থাকে। Unix জগতে এই ধারণা জনপ্রিয় হয় ১৯৮০-এর দশকের শেষ দিকে SunOS-এর mmap-ভিত্তিক shared library দিয়ে, পরে System V-এর ELF ফরম্যাটে; Linux ১৯৯০-এর দশকের মাঝামাঝি পুরোনো a.out থেকে ELF-এ সরে আসে।

কিন্তু dynamic linking-এর দামও আছে। প্রোগ্রাম চালু হতে symbol খোঁজায় সময় লাগে। আর সবচেয়ে বড় সমস্যা সংস্করণের গোলমাল: প্রোগ্রাম A-র লাগে library-র সংস্করণ ১, B-র লাগে ২, আর দুটি একই নামে একই জায়গায় থাকতে চায়। ১৯৯০-এর দশকের Windows-এ এর এত ভোগান্তি ছিল যে নাম হয়ে যায় “DLL hell” — কোনো installer একটি পুরোনো DLL দিয়ে নতুনটি মুছে দিলে অন্য প্রোগ্রাম ভেঙে যেত। পরে Windows সুরক্ষিত system file আর side-by-side assembly এনে, Linux soname (libc.so.6-এর “6”) আর symbol versioning (printf@GLIBC_2.2.5, যা উপরের nm আউটপুটে দেখেছেন) দিয়ে এর সমাধান করে।

Static binary এই গোলমাল এড়ায়: একটি ফাইল কপি করলেই চলে, কোন মেশিনে কোন library আছে তা নিয়ে ভাবতে হয় না। Go সাধারণত এভাবেই বানায় (C কোডের সাথে যুক্ত না হলে), Rust-এও এটি সহজ। Container image ছোট রাখতে আর “আমার মেশিনে চলে, সার্ভারে চলে না” দূর করতে এটি আকর্ষণীয়।

মূল উপলব্ধি: binding যত দেরিতে, নমনীয়তা তত বেশি কিন্তু অনিশ্চয়তাও তত বেশি; যত আগে, তত স্থির কিন্তু তত কঠিন বদলানো। Static বনাম dynamic linking এই চিরন্তন আপসেরই একটি রূপ।

প্র ০২প্রথম দিকের কম্পিউটার কেন compile-time বা load-time binding-এ সন্তুষ্ট ছিল, আর execution-time binding কীভাবে সর্বজনীন হলো?

প্রথম দিকের কম্পিউটারে একসাথে একটি প্রোগ্রামই চলত। তখন ঠিকানা compile বা assemble করার সময় ঠিক করে দিলেই চলত — প্রোগ্রাম প্রতিবার একই জায়গায় বসবে, অন্য কেউ সেখানে নেই। ছোট কম্পিউটারে এই ঐতিহ্য বহুদিন টিকেছিল: CP/M আর MS-DOS-এর .COM প্রোগ্রাম একটি নির্দিষ্ট offset (0x100) ধরে লেখা। বাড়তি হার্ডওয়্যার লাগত না, যা তখন খুব দামি ছিল।

Multiprogramming আসতেই সমস্যা হলো: একসাথে কয়েকটি প্রোগ্রাম মেমরিতে থাকলে কে কোথায় বসবে তা আগে থেকে জানা যায় না। প্রথম সমাধান ছিল সফটওয়্যারে — relocating loader, যা লোড করার সময় প্রোগ্রামের ঠিকানাগুলো ঠিক করে দিত। DOS-এর .EXE-র relocation table এরই উত্তরসূরি। কিন্তু load-time binding-এর একটি মৌলিক সীমা: একবার লোড হয়ে গেলে প্রোগ্রামের ভেতরের পয়েন্টারগুলো সেই ঠিকানায় বাঁধা, তাই প্রোগ্রামকে আর সরানো যায় না। প্রসেস swap out হয়ে ফিরে এলে আগের জায়গাতেই ফিরতে হবে, আর সেটি খালি না থাকলে অপেক্ষা।

হার্ডওয়্যার সমাধান এল ১৯৬০-এর দশকে। CDC 6600-এর মতো মেশিনে প্রতিটি প্রোগ্রামের জন্য একটি reference address আর field length register ছিল — এই পর্বের relocation আর limit register-এর প্রায় হুবহু রূপ। IBM System/360 আবার সুরক্ষার জন্য আলাদা পথ নিয়েছিল — মেমরির প্রতিটি ব্লকের সাথে একটি “storage key”, যা প্রসেসের key-র সাথে মিলতে হতো। ম্যানচেস্টারের Atlas কম্পিউটার ১৯৬২ সালের দিকে আরও এক ধাপ এগিয়ে paging ভিত্তিক virtual memory দেখায়, যা পর্ব ৩২ আর ৩৫-এর বিষয়।

Execution-time binding সর্বজনীন হলো কারণ চিপের দাম কমল, আর এর লাভ বহুমুখী: প্রসেসকে যেকোনো জায়গায় সরানো যায়, swap করে অন্য জায়গায় ফেরানো যায়, protection একই হার্ডওয়্যারে হয়ে যায়, আর পরে paging-এর মাধ্যমে মেমরি ভাগ করা ও ASLR সম্ভব হয়। আজ একটি সস্তা মোবাইল ফোনের CPU-তেও MMU আছে।

মূল উপলব্ধি: binding-এর ইতিহাস হলো সফটওয়্যারের কাজ হার্ডওয়্যারে সরানোর ইতিহাস। যখন হার্ডওয়্যার যথেষ্ট সস্তা হলো, প্রতিটি মেমরি-ব্যবহারে অনুবাদ করার “বিলাসিতা” হয়ে উঠল প্রয়োজন।

প্র ০৩iOS আর Android কেন ডেস্কটপের মতো ডিস্কে swap করে না? তাহলে RAM ফুরালে কী হয়?

ডেস্কটপের ধারণায় swap একটি নিরাপত্তা-জাল: RAM ফুরালে কম ব্যবহৃত মেমরি ডিস্কে যায়, সিস্টেম ধীর হয় কিন্তু কোনো প্রোগ্রাম মরে না। মোবাইলে এই জালের তিনটি সমস্যা। প্রথম, flash-এর আয়ু। ফোনের storage NAND flash, যার প্রতিটি ঘর সীমিত সংখ্যকবার মুছে লেখা যায়। ঘন ঘন swap সেই আয়ু খেয়ে ফেলে, আর ফোনের storage সাধারণত বদলানো যায় না। দ্বিতীয়, শক্তি। ডেটা storage-এ লেখা আর পড়া ব্যাটারি খরচ করে। তৃতীয়, অভিজ্ঞতা। ব্যবহারকারী আঙুল ছোঁয়ালে অ্যাপ সঙ্গে সঙ্গে সাড়া দেবে — swap থেকে মেমরি ফেরানোর অপেক্ষা ঝাঁকুনি তৈরি করে।

তাই মোবাইল OS অন্য পথ নিয়েছে। প্রথম ধাপ চেপে রাখা: Android-এ zram — RAM-এরই একটি অংশে compress করা swap, যেখানে ডেটা ডিস্কে না গিয়ে ছোট হয়ে RAM-এ থাকে; iOS আর macOS-এও memory compression আছে। CPU-র কিছু সময় খরচ হয়, কিন্তু flash বাঁচে। দ্বিতীয় ধাপ অনুরোধ: OS অ্যাপকে জানায় “মেমরি কম, cache ছেড়ে দিন” — iOS-এ memory warning, Android-এ onTrimMemory-এর মতো callback. তৃতীয় ধাপ বন্ধ করা: Android-এর lmkd বা iOS-এর Jetsam পেছনের, কম গুরুত্বপূর্ণ অ্যাপের প্রসেস একেবারে মেরে ফেলে।

এর ফলে মোবাইল অ্যাপের নকশাই বদলে গেছে। অ্যাপকে ধরে নিতে হয় যে পেছনে যাওয়ার পর সে যেকোনো সময় বিনা সতর্কতায় মারা যেতে পারে। তাই অবস্থা (কোন পাতায় ছিলেন, কী লিখছিলেন) আগেই সংরক্ষণ করতে হয়, যাতে ফিরে এলে নতুন প্রসেস সেটি পুনরুদ্ধার করে — ব্যবহারকারী টেরই পান না যে অ্যাপটি মাঝে মরে আবার জন্মেছে। এটি একধরনের “অ্যাপ-স্তরের swap”: kernel মেমরি-চিত্র লেখে না, অ্যাপ নিজে তার দরকারি অংশ লেখে।

অবস্থা বদলাচ্ছে: storage দ্রুত আর টেকসই হচ্ছে, তাই Apple কিছু নতুন iPad-এ সীমিত swap চালু করেছে। বাংলাদেশে প্রচলিত কম দামের, কম RAM-এর ফোনে পেছনের অ্যাপ বারবার নতুন করে চালু হওয়ার অভিজ্ঞতা অনেকেরই আছে — এটিই low-memory killer-এর কাজ।

মূল উপলব্ধি: একই সমস্যার (RAM কম) সমাধান হার্ডওয়্যারের সীমা অনুযায়ী বদলায়। ডেস্কটপ মেমরি ডিস্কে সরায়; মোবাইল মেমরি চাপে, ফেরত চায়, আর শেষে প্রসেসই সরিয়ে দেয়।

প্র ০৪Lazy binding শুরু দ্রুত করে — তবু অনেক distro কেন ডিফল্টভাবে BIND_NOW আর full RELRO বেছে নিয়েছে?

Lazy binding-এর যুক্তি পরিষ্কার: একটি বড় GUI প্রোগ্রাম কয়েক হাজার বাইরের ফাংশনের সাথে link হতে পারে, অথচ একবার চালানোয় তার সামান্য অংশই ডাকা হয়। শুরুতেই সব খুঁজলে চালু হতে সময় লাগে; lazy-তে শুধু যা ডাকা হয়, তার খোঁজার খরচ।

কিন্তু lazy binding-এর জন্য GOT-কে সারা জীবন লেখা যায় এমন রাখতে হয়, কারণ resolver যেকোনো সময় নতুন ঠিকানা লিখবে। আর GOT হলো ফাংশন-পয়েন্টারের একটি টেবিল — প্রোগ্রামের প্রতিটি বাইরের কল এখান থেকে ঠিকানা নেয়। কোনো মেমরি-বাগ দিয়ে যদি কেউ GOT-এর একটি ঘর বদলাতে পারে, পরের কলটি তার পছন্দের জায়গায় যাবে। এ কারণে নিরাপত্তা-গবেষণায় লেখা যায় এমন GOT একটি পরিচিত দুর্বল জায়গা হিসেবে আলোচিত।

প্রতিরক্ষা হিসেবে এল RELRO (relocation read-only). “Partial RELRO”-তে GOT-এর কিছু অংশ শুধু-পড়ার হয়, কিন্তু PLT-র ঘরগুলো lazy-র জন্য লেখা যায় এমন থাকে। “Full RELRO” = BIND_NOW + পুরো GOT শুধু-পড়ার: dynamic linker শুরুতে সব ঘর পূরণ করে, তারপর mprotect দিয়ে পাতাগুলো শুধু-পড়ার করে দেয়। এখন বাগ দিয়ে GOT বদলাতে গেলে প্রসেস SIGSEGV পেয়ে মারা যায় — অংশ ২-এর সেই হার্ডওয়্যার-দারোয়ানই, এবার পাতার অনুমতির রূপে।

দাম কী? চালু হওয়ার সময় সব symbol খোঁজা। আধুনিক মেশিনে, hash-ভিত্তিক দ্রুত খোঁজার কারণে, বেশিরভাগ প্রোগ্রামে এই খরচ মিলিসেকেন্ডের ঘরে বা তারও কম — তাই অনেক distro নিরাপত্তার পক্ষে ঝুঁকেছে। উপরের bash আউটপুটে Ubuntu 24.04-এর gcc ডিফল্টভাবেই BIND_NOW দিয়েছিল। নিজের সার্ভারের প্রোগ্রাম যাচাই করতে readelf -d prog | grep -E 'BIND_NOW|FLAGS' আর readelf -l prog | grep RELRO চালাতে পারেন।

মূল উপলব্ধি: পারফরম্যান্সের জন্য করা প্রতিটি “দেরিতে করা” কৌশল কিছু অবস্থা লেখা যায় এমন রাখে, আর লেখা যায় এমন অবস্থাই আক্রমণের জায়গা। আধুনিক সিস্টেম প্রায়ই কিছুটা গতি দিয়ে সেই জায়গা বন্ধ করে।

প্র ০৫Logical address space কি সবসময় physical-এর চেয়ে বড়? একটি 32-bit প্রসেস কেন ৪ GiB দেখে, অথচ কম্পিউটারে ৮ GiB RAM থাকতে পারে?

Logical আর physical address space দুটি স্বাধীন জিনিস, তাদের আকার আলাদা কারণে ঠিক হয়। Logical জগতের আকার ঠিক করে CPU-র ঠিকানার বিট-সংখ্যা — প্রোগ্রাম একটি পয়েন্টারে কত বড় সংখ্যা রাখতে পারে। Physical জগতের আকার ঠিক করে মেমরি-বাসের ঠিকানা-লাইন আর কত RAM বসানো আছে। মাঝের MMU দুটিকে মেলায়, তাই যেকোনোটি বড় হতে পারে।

Logical বড়, physical ছোট — আজকের সাধারণ ঘটনা। একটি 64-bit Linux প্রসেসের user অংশ সাধারণত ১২৮ TiB (৪৮-bit virtual ঠিকানার অর্ধেক), অথচ ল্যাপটপে হয়তো ৮ GiB RAM. এটি সম্ভব কারণ প্রসেস তার জগতের খুব সামান্য অংশই ব্যবহার করে, আর virtual memory (পর্ব ৩৫) দরকার না হলে physical মেমরি দেয়ই না।

Physical বড়, logical ছোট — ইতিহাসে বারবার ঘটেছে। DEC-এর PDP-11 পরিবার 16-bit — প্রতিটি প্রোগ্রাম মাত্র ৬৪ KiB দেখত — অথচ পরের মডেলে MMU দিয়ে ২২-bit physical ঠিকানা, অর্থাৎ ৪ MB পর্যন্ত RAM ব্যবহার করা যেত। একই গল্প x86-এ: 32-bit প্রসেস ২৩২ = ৪ GiB-এর বেশি দেখতে পারে না, কিন্তু ১৯৯৫ সালের Pentium Pro থেকে PAE (physical address extension) দিয়ে ৩৬-bit physical ঠিকানা — ৬৪ GiB পর্যন্ত RAM — সম্ভব হয়। তাই ৮ GiB RAM-এর 32-bit সার্ভারে প্রতিটি প্রসেস ৪ GiB-এর মধ্যে আটকা, কিন্তু অনেক প্রসেস মিলে পুরো ৮ GiB ব্যবহার করতে পারে। (32-bit Windows-এর client সংস্করণ driver-সামঞ্জস্যের কারণে মোট physical মেমরিও প্রায় ৪ GiB-এ সীমিত রাখত — OS-এর নীতি, হার্ডওয়্যারের সীমা নয়।)

মূল উপলব্ধি: MMU হলো একটি অনুবাদক, আর অনুবাদকের দুই দিকের ভাষার শব্দভান্ডার আলাদা হতেই পারে। পরীক্ষায় “logical address space-এর আকার” চাইলে ঠিকানার বিট গুনুন, RAM নয়।

অনুশীলন

হাতে হিসাব, আউটপুট অনুমান আর কমান্ড লেখা — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।

  1. হাতে হিসাব (base-limit আর relocation): (ক) একটি প্রসেসের base = 24000, limit = 6000. নিচের physical ঠিকানাগুলোর কোনটি বৈধ, কোনটিতে trap? 23999, 24000, 29999, 30000, 31500. (খ) আরেকটি সিস্টেমে MMU-তে relocation register = 52000, limit register = 8000. Logical ঠিকানা 0, 1200, 7999, 8000-এর physical ঠিকানা কত, বা trap?

    (ক) base + limit = ২৪০০০ + ৬০০০ = ৩০০০০, বৈধ পরিসর ২৪০০০ থেকে ২৯৯৯৯. তাই 23999 → trap (base-এর কম); 24000 → বৈধ; 29999 → বৈধ (শেষ বাইট); 30000 → trap (base + limit-এর সমান); 31500 → trap.

    (খ) আগে logical < 8000 যাচাই, তারপর ৫২০০০ যোগ: 0 → 52000; 1200 → 53200; 7999 → 59999; 8000 → trap (limit-এর সমান)। লক্ষ করুন, (খ)-তে limit-এর সাথে তুলনা হয় logical ঠিকানার, আর (ক)-তে physical ঠিকানার সাথে base ও base + limit-এর।

  2. হিসাব (swap): একটি সিস্টেমে backing store-এর transfer rate ২৫ MB/s আর গড় latency ১০ ms. (ক) ২০০ MB-র একটি প্রসেস swap out করে আরেকটি ৫০ MB-র প্রসেস swap in করতে মোট কত সময় লাগবে? (খ) একই transfer rate ২০০ Mb/s (মেগাবিট) লেখা থাকলে ২০০ MB প্রসেসের এক দিকের সময় কত?

    (ক) Swap out: ২০০ ÷ ২৫ = ৮ s = ৮০০০ ms, + ১০ ms = ৮০১০ ms. Swap in: ৫০ ÷ ২৫ = ২ s = ২০০০ ms, + ১০ ms = ২০১০ ms. মোট = ১০০২০ ms ≈ ১০.০২ সেকেন্ড. দুটি প্রসেসের আকার আলাদা বলে দুই দিকের সময় আলাদা — “২ দিয়ে গুণ” এখানে খাটে না।

    (খ) ২০০ Mb/s = ২০০ ÷ ৮ = ২৫ MB/s — অর্থাৎ (ক)-এর সমানই! তাই এক দিকে ৮ s (+ latency ১০ ms = ৮.০১ s). কেউ বিট-বাইট না মিলিয়ে ২০০ ÷ ২০০ = ১ s লিখলে ৮ গুণ ভুল।

  3. আউটপুট অনুমান: নিচের ফাইলটি gcc -c lib.c দিয়ে compile করে nm lib.o চালালে কোন চারটি নামের পাশে কোন অক্ষর আসবে (T, D, B বা U)? helper কেন তালিকায় অন্যরকম দেখাবে?
    #include <stdlib.h>
    int rate = 7;                 /* initialised global */
    int table[100];               /* zero global        */
    static int helper(int x) { return x + rate; }
    int compute(int n) { return helper(n) + table[0] + rand(); }

    একটি আসল রানে (gcc 13, অপটিমাইজেশন ছাড়া):

    0000000000000018 T compute
    0000000000000000 t helper
                     U rand
    0000000000000000 D rate
    0000000000000000 B table

    rate মান দেওয়া global → D; table শূন্য global → B (BSS); compute → T; rand এখানে সংজ্ঞায়িত নয় → U, linker তাকে libc থেকে খুঁজবে। helper static বলে ছোট হাতের t — local symbol, অন্য ফাইল একে দেখতে পায় না, তাই symbol resolution-এ অংশ নেয় না। compute-এর ঠিকানা 0x18, কারণ text-এ আগে helper-এর কোড বসেছে। (-O2 দিলে compiler helper-কে inline করে মুছে দেয়, তখন t helper লাইনটিই থাকে না। আর abs বা strlen-এর মতো কিছু ফাংশন gcc নিজেই built-in হিসেবে কোডে বসিয়ে দিতে পারে — তখন সেগুলো U তালিকায় আসে না; এ কারণেই উদাহরণে rand নেওয়া।)

  4. কমান্ড লিখুন: একটি Linux সার্ভারে /usr/local/bin/report প্রোগ্রাম সম্পর্কে জানতে চান: (ক) এটি কোন কোন shared library চায় — অচেনা ফাইল বলে ldd ছাড়া; (খ) এটি PIE কি না; (গ) এটি BIND_NOW দিয়ে তৈরি কি না; (ঘ) চলমান একটি report প্রসেসে (PID 4321) libc আসলে কোন ফাইল থেকে আর কোন অনুমতিতে map হয়েছে।

    (ক) readelf -d /usr/local/bin/report | grep NEEDED — ফাইল শুধু পড়ে, চালায় না। (ldd কিছু ক্ষেত্রে প্রোগ্রামের অংশ চালিয়ে ফেলতে পারে, তাই man ldd অবিশ্বস্ত ফাইলে এটি না চালাতে বলে।)

    (খ) readelf -h /usr/local/bin/report | grep Type — “DYN (Position-Independent Executable file)” হলে PIE, “EXEC” হলে নয়।

    (গ) readelf -d /usr/local/bin/report | grep -E 'BIND_NOW|FLAGS' — BIND_NOW বা Flags: NOW থাকলে হ্যাঁ।

    (ঘ) grep libc /proc/4321/maps — প্রতিটি লাইনে ঠিকানার পরিসর, অনুমতি (r-xp, rw-p…) আর ফাইলের পথ। অন্য ব্যবহারকারীর প্রসেস হলে অনুমতি লাগবে; sudo-র আগে ভাবুন সত্যিই দরকার কি না।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমDynamic linking-এ একটি C প্রোগ্রামের printf কল প্রথমবার চলা পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Compiler object file বানায়: call-এর লক্ষ্য ফাঁকা, সাথে relocation entry
  2. Linker নাম মেলায়, কলকে printf@plt-এ পাঠায় আর NEEDED-এ libc.so.6 লেখে
  3. execve-তে kernel প্রোগ্রাম আর dynamic linker-কে address space-এ map করে
  4. Dynamic linker libc.so.6 map করে আর GOT প্রস্তুত করে
  5. main চলে আর printf@plt-এর মাধ্যমে কল করে
  6. GOT-এর ঠিকানা ধরে libc-র printf চলে, MMU প্রতিটি ঠিকানা অনুবাদ করে

একটি প্রসেসের base register = 300040 আর limit register = 120900. CPU-র তৈরি নিচের কোন physical ঠিকানায় trap হবে?

  • 300040
  • 350000
  • 420940
  • 420939

একটি সরল MMU-তে relocation register-এর মান 14000 আর limit 3000. প্রোগ্রাম logical ঠিকানা 346 ব্যবহার করলে মেমরিতে কোন physical ঠিকানা যাবে?

  • 346
  • 14346
  • 13654
  • Trap — addressing error

কোন ধরনের address binding-এ একটি প্রসেসকে চলার মাঝখানে physical মেমরির অন্য জায়গায় সরানো যায়, আর এর জন্য বিশেষ হার্ডওয়্যার লাগে?

  • Compile-time binding
  • Load-time binding
  • Link-time binding
  • Execution-time binding

Linux-এ একটি object file-এ nm চালালে একটি নামের পাশে U দেখা গেল। এর মানে কী?

  • নামটি এই ফাইলে ব্যবহৃত কিন্তু সংজ্ঞায়িত নয়; linker অন্য কোথাও খুঁজবে
  • নামটি মান দেওয়া global ভ্যারিয়েবল
  • নামটি এই ফাইলে সংজ্ঞায়িত একটি ফাংশন
  • নামটি BSS-এর একটি ভ্যারিয়েবল

একটি shared library-কে কেন position-independent code (PIC) হিসেবে compile করা হয়?

  • যাতে কোড আরও দ্রুত চলে
  • যাতে কোড না বদলেই যেকোনো ঠিকানায় বসানো ও প্রসেসগুলোর মধ্যে ভাগ করা যায়
  • যাতে library-কে static link করা যায়
  • যাতে হার্ডওয়্যারের মেমরি-যাচাই এড়ানো যায়

Standard swapping-এ ১০০ MB-র একটি প্রসেস swap out করে একই আকারের আরেকটি প্রসেস swap in করতে হবে। Backing store-এর transfer rate ৫০ MB/s হলে (latency উপেক্ষা করে) মোট সময় কত?

  • ০.৫ সেকেন্ড
  • ২ সেকেন্ড
  • ৪ সেকেন্ড
  • ৫০০০ সেকেন্ড

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. CPU সরাসরি ছোঁয় শুধু register আর main memory; memory management-এর কাজ allocation, protection, relocation আর sharing — নীতি OS-এর, প্রতিটি ঠিকানার যাচাই হার্ডওয়্যারের।
  2. Base-limit: addr < base অথবা addr ≥ base + limit হলে trap; বৈধ পরিসর base থেকে base + limit − 1; register বসায় শুধু kernel.
  3. Binding-এর তিন সময়: compile (absolute code, যেমন .COM), load (relocatable code, যেমন .EXE), execution (MMU; আজকের নিয়ম)। শেষটিতেই logical ≠ physical: physical = logical + relocation, যেমন 346 + 14000 = 14346.
  4. Compile → link → load: object file-এ U symbol আর relocation entry; linker নাম মেলায়; dynamic linker shared library আনে, PLT/GOT দিয়ে কল পৌঁছায় (lazy বা BIND_NOW); dlopen দিয়ে চলার সময় আনা যায়; PIC/PIE ছাড়া ASLR পূর্ণ হয় না।
  5. Swap time = আকার ÷ গতি (+ latency), দুই দিকে; আধুনিক OS swap করে page ধরে, মোবাইল OS চাপে (compression) আর অ্যাপ বন্ধ করে; overlay ছিল প্রোগ্রামারের হাতে-করা পুরোনো সমাধান।

পরের পর্বে: Base আর limit দিয়ে প্রতিটি প্রসেসকে RAM-এর একটি টানা অংশ দিলে নতুন প্রসেস কোন ফাঁকা জায়গায় বসবে — প্রথম যেটি পাওয়া যায়, সবচেয়ে মানানসই, নাকি সবচেয়ে বড়? আর আসা-যাওয়ায় তৈরি ছোট ছোট ফাঁকা জায়গা (fragmentation) কীভাবে মেমরি নষ্ট করে, buddy system কীভাবে তা সামলায় — contiguous allocation-এর পুরো হিসাব।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান