এই পর্বে যা শিখবেন
- প্রোগ্রামারের চোখে মেমরির segment-ভিত্তিক ছবি, আর ⟨segment, offset⟩ logical address
- Segment table, STBR/STLR আর limit-check — পাঠ্যবইয়ের পাঁচ segment-এর উদাহরণে প্রতিটি ঠিকানার পূর্ণ হিসাব, trap-সহ
- Segment-ভিত্তিক protection আর sharing, external fragmentation, আর paging-এর সাথে তুলনা
- MULTICS আর Intel 386-এর segmentation + paging, 8086-এর real mode, A20-এর গল্প, 80286-এর protected mode আর আধুনিক flat model
- x86-64-এ FS/GS কীভাবে thread-local storage আর per-CPU ডেটার কাজে লাগে — চালানো যায় এমন C কোডে নিজে দেখে, আর কেন শেষ পর্যন্ত paging জিতল
১ · প্রোগ্রামারের চোখে মেমরি — কয়েকটি আলাদা টুকরো
পর্ব ৩২-এ paging দেখেছেন: প্রসেসের ঠিকানা-জগৎকে (address space) সমান মাপের page-এ কেটে, যেকোনো খালি frame-এ বসানো। Paging হার্ডওয়্যারের জন্য চমৎকার, কিন্তু এটি প্রোগ্রামের অর্থ জানে না। একটি page-এর অর্ধেকে হয়তো একটি function-এর শেষ অংশ, বাকি অর্ধেকে একটি global array-এর শুরু। হার্ডওয়্যারের কাছে দুটোই শুধু ৪০৯৬ বাইট।
কিন্তু প্রোগ্রামার নিজের প্রোগ্রামকে এভাবে দেখেন না। একটি C প্রোগ্রাম লেখার সময় আপনি ভাবেন: এখানে main আর অন্য function-এর কোড, ওখানে global ভেরিয়েবল, function call-এর জন্য একটি stack, malloc-এর জন্য একটি heap, আর printf-এর মতো library function-এর কোড। পর্ব ৮-এ প্রসেসের memory layout-এ ঠিক এই ভাগগুলোই দেখেছিলেন — text, data, bss, heap আর stack. প্রতিটি ভাগের মাপ আলাদা, আর প্রতিটির আচরণও আলাদা: code বদলায় না, stack বাড়ে-কমে, heap দরকারমতো বড় হয়।
একটি সরকারি অফিসের ফাইল ক্যাবিনেটের কথা ভাবুন। ক্যাবিনেটে কয়েকটি ড্রয়ার — একটিতে “নিয়োগ”, একটিতে “হিসাব”, একটিতে “চিঠিপত্র”। কেউ কোনো কাগজ চাইলে বলেন না “ক্যাবিনেটের ৮৪৩ নম্বর কাগজটি দিন”; বলেন “হিসাব ড্রয়ারের ১২ নম্বর ফাইল”। ড্রয়ারের নাম আর ভেতরের অবস্থান — এই দুই মিলে ঠিকানা। প্রতিটি ড্রয়ারের মাপ আলাদা হতে পারে, আর প্রতিটি ড্রয়ারের তালার নিয়মও আলাদা: “হিসাব” ড্রয়ার শুধু হিসাবরক্ষক খুলতে পারেন, “চিঠিপত্র” সবাই পড়তে পারেন। Segmentation এই চিন্তাকেই মেমরিতে আনে: segment = ড্রয়ার, offset = ড্রয়ারের ভেতরে অবস্থান, segment table = কোন ড্রয়ার আলমারির কোথায় আর কত বড়, তার তালিকা।
তাই segmentation-এ প্রসেসের logical ঠিকানা-জগৎ হলো কয়েকটি segment-এর সংগ্রহ। প্রতিটি segment-এর একটি নম্বর (বা নাম) আর একটি দৈর্ঘ্য। একটি logical address তাই দুই অংশের জোড়া:
s = segment number · d = offset (segment-এর শুরু থেকে কত বাইট দূরে)
Paging-এ logical address-ও দুই ভাগে ভাঙে (page নম্বর আর offset), কিন্তু সেখানে ভাগটি হার্ডওয়্যার নিজে করে — ঠিকানার উপরের bit-গুলো page নম্বর, নিচের ১২টি offset, প্রোগ্রামার তা টেরও পান না। Segmentation-এ পার্থক্য হলো: segment নম্বর আর offset প্রোগ্রামের কাছে দৃশ্যমান দুটি আলাদা জিনিস, আর offset-এর সর্বোচ্চ মান segment-এর নিজের দৈর্ঘ্য — কোনো ধ্রুব page-মাপ নয়।
কে segment বানায়? সাধারণত compiler আর linker. একটি C compiler ঐতিহাসিকভাবে আলাদা segment তৈরি করতে পারত: কোডের জন্য একটি, global ভেরিয়েবলের জন্য একটি, প্রতিটি thread-এর stack-এর জন্য একটি, heap-এর জন্য একটি, আর প্রতিটি shared library-র জন্য আলাদা। Loader তারপর প্রতিটিকে segment নম্বর দেয়। একটি ছোট প্রোগ্রামের সম্ভাব্য segment-গুলো এমন হতে পারে:
| Segment নম্বর | কী থাকে | মাপ বদলায়? | স্বাভাবিক অধিকার |
|---|---|---|---|
| ০ | প্রোগ্রামের code (main, অন্য function) | না | পড়া + চালানো |
| ১ | global আর static ডেটা | না | পড়া + লেখা |
| ২ | stack | হ্যাঁ, call-এর গভীরতা অনুযায়ী | পড়া + লেখা |
| ৩ | heap | হ্যাঁ, malloc অনুযায়ী | পড়া + লেখা |
| ৪ | C library-র code (shared) | না | পড়া + চালানো, অন্য প্রসেসের সাথে ভাগ |
শেষ কলামটি লক্ষ করুন। অধিকার segment-এর অর্থের সাথে মেলে, তাই প্রতিটি segment-এ একবার অধিকার বসালেই পুরোটা সুরক্ষিত। এটিই segmentation-এর সবচেয়ে বড় আকর্ষণ, যা অংশ ৩-এ দেখবেন। তার আগে দেখি হার্ডওয়্যার ⟨s, d⟩ থেকে physical ঠিকানা কীভাবে বের করে।
২ · Segment table, base আর limit — হার্ডওয়্যারের অনুবাদ
পর্ব ৩০-এ একটি base register আর একটি limit register দিয়ে পুরো প্রসেসকে রক্ষা করা দেখেছেন: প্রতিটি ঠিকানা limit-এর কম কি না পরীক্ষা, তারপর base যোগ। Segmentation সেই ধারণাটিকেই বহুবচনে নিয়ে যায় — প্রতিটি segment-এর জন্য একজোড়া base আর limit. এই জোড়াগুলোর তালিকাই segment table:
- Base: segment-টি physical memory-র কোন ঠিকানা থেকে শুরু।
- Limit: segment-টি কত বাইট লম্বা। বৈধ offset তাই ০ থেকে limit − ১ পর্যন্ত।
- বাস্তব হার্ডওয়্যারে প্রতিটি entry-তে আরও থাকে protection bit (পড়া/লেখা/চালানো), একটি valid bit, আর কখনো privilege level.
Segment table থাকে মেমরিতে। হার্ডওয়্যার তাকে খুঁজে পায় দুটি register দিয়ে: STBR (Segment-Table Base Register) — table-টি মেমরির কোথায় শুরু; আর STLR (Segment-Table Length Register) — প্রসেসটির কতটি segment আছে। প্রতিটি প্রসেসের নিজের segment table, তাই context switch-এর সময় OS এই দুই register নতুন প্রসেসের মানে বদলে দেয় (মান দুটি থাকে প্রসেসের PCB-তে), ঠিক যেমন paging-এ page table-এর ঠিকানা বদলায়।
Logical address ⟨s, d⟩ এলে MMU: (১) s < STLR? না হলে trap — এমন segment প্রসেসের নেই। (২) table থেকে entry s পড়ে base আর limit নেয় (entry-র ঠিকানা = STBR + s × entry-মাপ)। (৩) d < limit? না হলে trap — segment-এর বাইরে যাওয়ার চেষ্টা। (৪) physical address = base + d. লক্ষ করুন, পরীক্ষাটি “d < limit”, “d ≤ limit” নয়; limit হলো দৈর্ঘ্য, তাই d = limit ঠিক এক বাইট বাইরে।
Trap মানে হার্ডওয়্যার নির্দেশটি থামিয়ে kernel-এর একটি handler চালায় (পর্ব ৪-এ trap আর exception দেখেছেন)। সাধারণত OS তখন প্রসেসটিকে বন্ধ করে। Unix-ধাঁচের সিস্টেমে এর নাম আজও segmentation fault — signal SIGSEGV. মজার ব্যাপার, আজকের Linux-এ এই ত্রুটি প্রায় সবসময় আসে paging-এর পরীক্ষা থেকে (অবৈধ page), segment থেকে নয়; নামটি শুধু ইতিহাসের স্মৃতি।
পাঠ্যবইয়ের উদাহরণ — পাঁচটি segment
বিশ্ববিদ্যালয় আর চাকরির পরীক্ষায় খুব পরিচিত একটি অনুশীলনী। একটি প্রসেসের segment table:
| Segment | Base | Limit (দৈর্ঘ্য) | বৈধ physical পরিসর |
|---|---|---|---|
| ০ | ২১৯ | ৬০০ | ২১৯ – ৮১৮ |
| ১ | ২৩০০ | ১৪ | ২৩০০ – ২৩১৩ |
| ২ | ৯০ | ১০০ | ৯০ – ১৮৯ |
| ৩ | ১৩২৭ | ৫৮০ | ১৩২৭ – ১৯০৬ |
| ৪ | ১৯৫২ | ৯৬ | ১৯৫২ – ২০৪৭ |
প্রশ্ন: নিচের logical address-গুলোর physical address কত? ⟨০, ৪৩০⟩, ⟨১, ১০⟩, ⟨২, ৫০০⟩, ⟨৩, ৪০০⟩, ⟨৪, ১১২⟩। সাথে আরেকটি যোগ করছি — ⟨৫, ২০⟩ — যাতে STLR-এর পরীক্ষাও দেখা যায়। প্রথমে নিজে খাতায় করুন, তারপর stepper চালান।
1 STBR -> table, STLR = 52 (0,430): 0 < 5, 430 < 600 -> 219+4303 (1,10): 1 < 5, 10 < 14 -> 2300+104 (2,500): 2 < 5, 500 >= 100 -> TRAP5 (3,400): 3 < 5, 400 < 580 -> 1327+4006 (4,112): 4 < 5, 112 >= 96 -> TRAP7 (5,20): 5 >= 5 -> TRAP (no segment 5)
| Logical ⟨s, d⟩ | |
| s < STLR? | |
| Base, limit | |
| d < limit? | |
| ফল |
- শুরু। Context switch-এর সময় OS এই প্রসেসের STBR আর STLR বসিয়েছে: table মেমরির কোথায় আর এতে ৫টি entry. ডানে physical memory-তে segment-গুলো ঠিকানা অনুযায়ী সাজানো — খেয়াল করুন, segment নম্বরের ক্রম আর physical ক্রম এক নয়।
- ⟨০, ৪৩০⟩। ০ < ৫, তাই entry ০ পড়া হয়: base ২১৯, limit ৬০০। ৪৩০ < ৬০০, বৈধ। Physical = ২১৯ + ৪৩০ = ৬৪৯.
- ⟨১, ১০⟩। Segment ১ মাত্র ১৪ বাইটের, কিন্তু ১০ < ১৪, তাই বৈধ। Physical = ২৩০০ + ১০ = ২৩১০.
- ⟨২, ৫০০⟩ — trap. Segment ২-এর দৈর্ঘ্য ১০০, অথচ offset ৫০০। যোগ করলে ৯০ + ৫০০ = ৫৯০ হতো, যা segment ০-এর এলাকায় পড়ে — অন্য segment-এর ডেটা। Limit-পরীক্ষা ঠিক এই অনুপ্রবেশ আটকায়: trap (addressing error).
- ⟨৩, ৪০০⟩। ৪০০ < ৫৮০, বৈধ। Physical = ১৩২৭ + ৪০০ = ১৭২৭.
- ⟨৪, ১১২⟩ — trap. Segment ৪-এর দৈর্ঘ্য ৯৬, বৈধ offset ০–৯৫। ১১২ ≥ ৯৬, তাই trap. পাস করতে দিলে ১৯৫২ + ১১২ = ২০৬৪ — যা কোনো segment-এর নয়।
- ⟨৫, ২০⟩ — অন্য ধরনের trap. ৫ ≥ STLR (৫)। Segment ০–৪ আছে, ৫ নেই; table-এ ৫ নম্বর entry পড়ার চেষ্টাই হয় না, কারণ STBR + ৫ × entry-মাপে হয়তো অন্য কোনো ডেটা আছে। এই পরীক্ষা না থাকলে প্রসেস নিজের table-এর বাইরে থেকে ভুয়া base-limit পড়িয়ে নিত।
নিচের simulator একই কাজ করে। Table বা ঠিকানা বদলে বইয়ের যেকোনো অনুশীলনী যাচাই করতে পারেন — শেষ দুটি ঠিকানা off-by-one বোঝার জন্য রাখা।
// Segment-table translation: <segment, offset> -> physical address, or a trap.
const table = [ // [base, limit] for segments 0..4
[219, 600], [2300, 14], [90, 100], [1327, 580], [1952, 96]
];
const STLR = table.length; // segment-table length register: 5 segments
function translate(s, d) {
const tag = "<" + s + "," + d + "> ";
if (s >= STLR) return tag + "trap: segment " + s + " >= STLR " + STLR;
const [base, limit] = table[s];
if (d >= limit) return tag + "trap: offset " + d + " >= limit " + limit;
return tag + "-> " + base + " + " + d + " = " + (base + d);
}
const refs = [[0, 430], [1, 10], [2, 500], [3, 400], [4, 112], [5, 20], [1, 13], [1, 14]];
for (const [s, d] of refs) console.log(translate(s, d));আউটপুট (নির্ধারিত, প্রতিবার একই):
<0,430> -> 219 + 430 = 649
<1,10> -> 2300 + 10 = 2310
<2,500> trap: offset 500 >= limit 100
<3,400> -> 1327 + 400 = 1727
<4,112> trap: offset 112 >= limit 96
<5,20> trap: segment 5 >= STLR 5
<1,13> -> 2300 + 13 = 2313
<1,14> trap: offset 14 >= limit 14
প্রথম পাঁচ লাইন পাঠ্যবইয়ের উত্তর: ৬৪৯, ২৩১০, trap, ১৭২৭, trap. ষষ্ঠ লাইনে STLR-পরীক্ষা: segment ৫ নেই। শেষ দুই লাইন সবচেয়ে শিক্ষণীয়: segment ১-এর দৈর্ঘ্য ১৪, তাই offset ১৩ শেষ বৈধ বাইট (২৩১৩), আর offset ১৪-তেই trap. পরীক্ষার খাতায় একটি সাধারণ ভুল ঠিক এখানে — কেউ “d ≤ limit” লিখে ⟨১, ১৪⟩-কে ২৩১৪ বলে দেন। কোডে দুই পরীক্ষার ক্রমও লক্ষ করুন: আগে s >= STLR, তারপরই table[s] পড়া — উল্টো ক্রমে লিখলে JavaScript-এ undefined আসত, আর হার্ডওয়্যারে অবৈধ মেমরি পড়া হতো।
খরচের কথা: প্রতিটি মেমরি-ব্যবহারে যদি আগে segment table পড়তে মেমরিতে যেতে হয়, তাহলে প্রতিটি load দুবার মেমরিতে যায় — paging-এর মতোই সমস্যা। সমাধানও একই ধাঁচের: segment entry CPU-র ভেতরে রেখে দেওয়া। Paging-এ সেটি TLB (পর্ব ৩৩)। x86-এ segment-এর জন্য আলাদা ব্যবস্থা আছে — segment register-এর লুকানো অংশ, যা অংশ ৫-এ দেখবেন। Segment সংখ্যায় কম (কয়েকটি থেকে কয়েক ডজন), তাই এদের cache করা page-এর চেয়ে অনেক সহজ।
৩ · প্রতিটি segment-এর নিজের তালা — protection আর sharing
Segment যেহেতু প্রোগ্রামের অর্থপূর্ণ একক, তাই প্রতিটি segment-এ আলাদা protection bit বসানো স্বাভাবিক। Code segment-এ “পড়া + চালানো, লেখা নিষেধ” দিলে কোনো বাগ বা আক্রমণ নিজের কোড বদলাতে পারে না; data আর stack segment-এ “পড়া + লেখা, চালানো নিষেধ” দিলে কেউ ডেটা হিসেবে ঢোকানো বাইট চালাতে পারে না। একটি array-কে নিজের segment-এ রাখলে limit-পরীক্ষা নিজেই array-র সীমা পাহারা দেয় — array-র বাইরে লেখার চেষ্টা হার্ডওয়্যারই ধরে ফেলে।
Paging-এও প্রতিটি page-এ এমন bit থাকে (পর্ব ৩২)। পার্থক্য হলো সূক্ষ্মতায়: page-এর সীমানা আর প্রোগ্রামের অর্থের সীমানা মেলে না। একটি ৬০০ বাইটের array একটি page-এর মাঝখানে বসতে পারে, তার পাশেই অন্য ডেটা; page-এর bit দিয়ে array-র শেষ সীমা পাহারা দেওয়া যায় না। Segment-এ সীমানাটি বাইট পর্যন্ত নিখুঁত।
Sharing — একটি code segment, অনেক প্রসেস
ধরুন একটি কম্পিউটারে ৪০ জন একই text editor চালাচ্ছেন। Editor-এর code ১৫০ KB, প্রত্যেকের নিজের ডেটা ৫০ KB. আলাদা কপি রাখলে লাগে ৪০ × ২০০ = ৮০০০ KB. কিন্তু code কেউ বদলায় না, তাই একটি কপিই যথেষ্ট: ১৫০ + ৪০ × ৫০ = ২১৫০ KB. Segmentation-এ এই sharing খুবই সহজ — প্রত্যেক প্রসেসের segment table-এর code entry-তে একই base আর limit বসিয়ে দিলেই হলো, অধিকার “শুধু পড়া + চালানো”। প্রত্যেকের data segment-এর entry আলাদা base-এ।
| প্রসেস | Segment ০ (editor code) | Segment ১ (নিজের ডেটা) |
|---|---|---|
| P1 | base ৪৩০৬২, limit ২৫২৮৬, R+X | base ৬৮৩৪৮, limit ৪৪২৫, R+W |
| P2 | base ৪৩০৬২, limit ২৫২৮৬, R+X (একই!) | base ৯০০০৩, limit ৮৮৫০, R+W |
Paging-এও sharing হয় (একই frame দুই প্রসেসের page table-এ), কিন্তু সেখানে প্রতিটি page আলাদাভাবে ভাগ করতে হয়; ১৫০ KB মানে ৩৮টি page entry. Segmentation-এ একটিই entry. আজকের Linux-এ shared library (যেমন libc.so) ঠিক এভাবেই ভাগ হয়, তবে page ধরে — আর mmap দিয়ে (পর্ব ৩৮)।
একটি সূক্ষ্ম ফাঁদ: shared code segment যদি নিজের ভেতরের কোনো ঠিকানা ⟨s, d⟩ আকারে উল্লেখ করে (যেমন একটি jump), তাহলে সব প্রসেসে ওই segment-কে একই নম্বর দিতে হয়। P1-এ editor-এর code segment ০ আর P2-তে segment ৪ হলে “⟨০, ১২০⟩-এ jump” নির্দেশটি P2-তে ভুল জায়গায় যাবে। সমাধান: code-এর ভেতরে শুধু offset-ভিত্তিক (relative) ঠিকানা, বা segment register-এর মাধ্যমে পরোক্ষ ঠিকানা ব্যবহার করা। আজকের position-independent code-এর পেছনের চিন্তাও একই — একটি library যেকোনো ঠিকানায় বসেও যেন ঠিক চলে।
৪ · External fragmentation ফিরে আসে — paging-এর সাথে তুলনা
Segmentation-এর দাম এখানে। প্রতিটি segment physical memory-তে একটি অবিচ্ছিন্ন (contiguous) টুকরো চায়, আর segment-গুলোর মাপ ভিন্ন। পর্ব ৩১-এ contiguous allocation-এ ঠিক এই পরিস্থিতিই দেখেছেন: প্রসেস আসে-যায়, মাঝে মাঝে ফাঁকা গর্ত (hole) তৈরি হয়, আর একসময় মোট খালি জায়গা যথেষ্ট হলেও কোনো একটি গর্তে নতুন segment আঁটে না। এর নাম external fragmentation. Segmentation-এ সমস্যাটি পুরো প্রসেসের বদলে segment-এর স্তরে চলে আসে — টুকরো ছোট বলে কিছুটা কম, কিন্তু মূলে একই।
ঢাকার একটি ব্যস্ত রাস্তার পাশে গাড়ি পার্কিং ভাবুন, যেখানে দাগ টানা স্লট নেই। কেউ ছোট গাড়ি রাখেন, কেউ মাইক্রোবাস। গাড়ি চলে গেলে মাঝে মাঝে ফাঁকা জায়গা থাকে — একটি মোটরসাইকেলের জায়গা এখানে, একটি প্রাইভেট কারের জায়গা ওখানে। সব ফাঁকা জায়গা যোগ করলে হয়তো একটি বাসের জায়গা হয়, কিন্তু কোনো একটি ফাঁকা অংশে বাস ঢোকে না। এটাই external fragmentation. দাগ টানা সমান স্লট (paging) এই সমস্যা দূর করে, কিন্তু তখন একটি মোটরসাইকেলও পুরো স্লট নেয় — সেটি internal fragmentation.
পাঠ্যবইয়ের আরেকটি পরিচিত segment table দেখুন: segment ০ (base ১৪০০, limit ১০০০), ১ (৬৩০০, ৪০০), ২ (৪৩০০, ৪০০), ৩ (৩২০০, ১১০০), ৪ (৪৭০০, ১০০০)। Physical ঠিকানা অনুযায়ী সাজালে segment-গুলো দখল করে ১৪০০–২৩৯৯, ৩২০০–৪২৯৯, ৪৩০০–৪৬৯৯, ৪৭০০–৫৬৯৯ আর ৬৩০০–৬৬৯৯। ফাঁকে দুটি গর্ত: ২৪০০–৩১৯৯ (৮০০ বাইট) আর ৫৭০০–৬২৯৯ (৬০০ বাইট)। ধরুন ১৪০০-এর নিচে OS নিজে বসে আছে, আর ৬৭০০-এর ওপরে অন্য প্রসেস। তাহলে মোট ১৪০০ বাইট খালি, তবু ১০০০ বাইটের একটি নতুন segment কোথাও আঁটে না।
প্রতিকার পর্ব ৩১-এর মতোই: first-fit বা best-fit দিয়ে গর্ত বেছে নেওয়া, আর দরকার হলে compaction — segment-গুলো সরিয়ে এক পাশে জড়ো করা, যাতে সব গর্ত মিলে একটি বড় গর্ত হয়। Segmentation-এ compaction সম্ভব, কারণ segment সরালে শুধু segment table-এর base বদলাতে হয়; প্রোগ্রামের ঠিকানা ⟨s, d⟩ একই থাকে। কিন্তু মেগাবাইট-গিগাবাইট ডেটা কপি করা ধীর, আর সেই সময় প্রসেস থামিয়ে রাখতে হয়।
| বৈশিষ্ট্য | Paging (পর্ব ৩২) | Segmentation |
|---|---|---|
| টুকরোর মাপ | সব সমান (যেমন ৪ KiB) | ভিন্ন ভিন্ন, প্রোগ্রাম অনুযায়ী |
| প্রোগ্রামার দেখেন? | না — ঠিকানা একটি লম্বা সংখ্যা | হ্যাঁ — ⟨segment, offset⟩ |
| External fragmentation | নেই — যেকোনো খালি frame চলে | আছে — segment-এর জন্য অবিচ্ছিন্ন জায়গা লাগে |
| Internal fragmentation | আছে — শেষ page গড়ে অর্ধেক খালি | প্রায় নেই — segment ঠিক যতটা দরকার |
| Protection-এর একক | page (অর্থহীন সীমানা) | segment (অর্থপূর্ণ, বাইট-নিখুঁত সীমানা) |
| Sharing | page ধরে ধরে | পুরো segment একটি entry-তে |
| Table-এর মাপ | বড় (অনেক page) — পর্ব ৩৩-এর multilevel table লাগে | ছোট (কয়েকটি segment) |
| Disk-এ সরানো (swap) | সহজ — সমান মাপের টুকরো | কঠিন — বড় segment পুরোটা সরাতে হয় |
টেবিলটি পড়লেই বোঝা যায় দুই পদ্ধতি একে অপরের পরিপূরক: segmentation প্রোগ্রামারের সুবিধা দেয়, paging মেমরি-ব্যবস্থাপকের। তাই ১৯৬০-এর দশকেই প্রশ্ন উঠেছিল — দুটো একসাথে করা যায় না?
৫ · দুটো একসাথে — MULTICS আর Intel 386
MULTICS (Multiplexed Information and Computing Service) প্রকল্প শুরু হয় ১৯৬০-এর দশকের মাঝামাঝি — MIT, Bell Labs আর General Electric মিলে; চলত GE-645 কম্পিউটারে। এর মূল নকশায় প্রতিটি segment নিজেই page-এ ভাগ করা ছিল (segmented paging): প্রতিটি segment-এর নিজের page table, তাই বড় segment-কেও অবিচ্ছিন্ন জায়গা লাগত না। MULTICS আরও এক ধাপ এগিয়েছিল: ফাইল আর segment-কে এক করে দেখা — একটি ফাইল খুললে সেটি একটি segment হয়ে ঠিকানা-জগতে চলে আসে। Bell Labs পরে প্রকল্প ছেড়ে দেয়, আর সেখানকার Ken Thompson ও Dennis Ritchie অনেক সরল একটি সিস্টেম লেখেন — Unix (পর্ব ২)। MULTICS-এর protection ring-এর ধারণা পরে x86-এর ring 0–3-এ ফিরে আসে।
Segmentation + paging-এ ঠিকানা দুই ধাপে অনুবাদ হয়। প্রথমে segment-এর base আর limit দিয়ে একটি মধ্যবর্তী ঠিকানা; তারপর সেই ঠিকানা paging দিয়ে physical-এ। এতে segmentation-এর অর্থপূর্ণ protection থাকে, আর paging-এর কারণে external fragmentation চলে যায়। দাম: দুই স্তরের table আর হার্ডওয়্যারের জটিলতা।
Intel 80386 — selector থেকে physical ঠিকানা
বাস্তবে সবচেয়ে বেশি ছড়ানো segmentation + paging হলো Intel 80386 (১৯৮৫)-এর protected mode. এখানে পরিভাষাগুলো একটু আলাদা:
- Segment register: CPU-তে ছয়টি — CS (code), DS (data), SS (stack), ES, FS, GS. প্রতিটি নির্দেশ কোনো একটি segment register ব্যবহার করে; যেমন instruction fetch সবসময় CS দিয়ে, stack-এর push/pop SS দিয়ে।
- Selector: segment register-এ রাখা ১৬-bit মান। এর তিন অংশ: index (১৩ bit) — table-এর কোন entry; TI (১ bit) — ০ হলে GDT (Global Descriptor Table, সবার জন্য এক), ১ হলে LDT (Local Descriptor Table, প্রসেস-নিজস্ব হতে পারে); আর RPL (২ bit) — অনুরোধকারীর privilege level.
- Descriptor: GDT বা LDT-র ৮ বাইটের একটি entry — এটিই পাঠ্যবইয়ের segment table entry. এতে ৩২-bit base, ২০-bit limit, একটি granularity bit (G = ১ হলে limit ৪ KiB-এর এককে, ফলে সর্বোচ্চ ৪ GiB), present bit, ধরন (code/data, পড়া/লেখা) আর DPL — segment-টি ব্যবহার করতে ন্যূনতম কোন privilege লাগবে।
- GDTR আর LDTR: GDT আর LDT মেমরির কোথায় — পাঠ্যবইয়ের STBR/STLR-এর x86 রূপ। এগুলো বসানোর নির্দেশ (
LGDT,LLDT) privileged, শুধু kernel চালাতে পারে। - Linear address: base + offset — segmentation-এর ফল, paging-এর ইনপুট। Paging বন্ধ থাকলে এটিই physical ঠিকানা।
একটি উদাহরণ ধরে পুরো পথটি দেখা যাক। হিসাব সহজ রাখতে ধরছি DS-এর segment-এর base 0x00400000 — বাস্তব আধুনিক OS-এ base 0 থাকে (অংশ ৭), কিন্তু শূন্য-ছাড়া base দিলে segmentation-এর যোগটি চোখে পড়ে। আর paging ৩২-bit-এর ক্লাসিক দুই স্তরের (১০ + ১০ + ১২ bit), যা পর্ব ৩৩-এ দেখেছেন।
1 mov eax, [0x00012345] ; uses DS2 DS = 0x0010 -> index 2, TI 0, RPL 03 GDT[2]: base 0x00400000, limit 0xFFFFF, G=14 0x00012345 <= limit -> linear 0x004123455 linear = dir 1 : table 0x012 : off 0x3456 PTE -> frame 0x0ABCD -> phys 0x0ABCD345
| ধাপ | |
| Offset (effective) | |
| Selector | |
| Base / limit | |
| Linear | |
| Physical |
- নির্দেশ। প্রোগ্রাম মেমরির
0x00012345offset থেকে পড়তে চায়। কোনো segment উল্লেখ না থাকলে ডেটার জন্য CPU ধরে নেয় DS. এই offset-কে Intel বলে effective address — পাঠ্যবইয়ের d. - Selector ভাঙা। DS-এ আছে
0x0010= বাইনারি ০০০০০০০০০০০১০ ০ ০০। নিচের দুই bit RPL = ০, তার উপরের bit TI = ০ (GDT), বাকি ১৩ bit index = ২। অর্থাৎ “GDT-র ২ নম্বর entry, kernel-স্তরের অনুরোধ”। - Descriptor. Entry-র ঠিকানা GDTR-এর base + ২ × ৮। কিন্তু বাস্তবে CPU প্রতিবার এটি মেমরি থেকে পড়ে না: DS-এ selector বসানোর মুহূর্তেই descriptor পড়ে register-এর একটি লুকানো অংশে (descriptor cache) রেখে দেয়, আর তখনই present bit আর privilege পরীক্ষা করে। এখানে base
0x00400000, limit0xFFFFF, G = ১ — তাই কার্যকর limit ৪ GiB − ১। - Segmentation-এর ফল। Offset limit-এর মধ্যে, তাই ঠিক পাঠ্যবইয়ের মতো যোগ: linear =
0x00400000+0x00012345=0x00412345. Limit পেরোলে এখানেই general protection fault হতো (stack segment-এর বেলায় stack fault). - Paging-এর ভাগ। এবার linear ঠিকানা পর্ব ৩৩-এর দুই স্তরের paging-এ যায়: উপরের ১০ bit = page directory index ১, মাঝের ১০ bit = page table index
0x012(দশমিকে ১৮), নিচের ১২ bit = offset0x345. CR3 দেখায় page directory কোথায়। (TLB-তে থাকলে এই হাঁটা লাগে না।) - Physical. ধরে নিই page table entry বলছে frame
0x0ABCD. Physical = frame × ৪০৯৬ + offset =0x0ABCD345. Segment পরীক্ষা, page পরীক্ষা — দুই স্তরের সুরক্ষা পেরিয়ে তবেই মেমরি।
খেয়াল করুন, 386-এর segmentation আর পাঠ্যবইয়ের segmentation-এ একটি বড় পার্থক্য: পাঠ্যবইয়ে segment নম্বর ঠিকানার অংশ, প্রতিটি pointer-এ লেখা থাকে। 386-এ segment নম্বর (selector) থাকে register-এ, আর সাধারণ pointer শুধু ৩২-bit offset. প্রোগ্রাম চাইলে ৪৮-bit “far pointer” (selector + offset) ব্যবহার করতে পারে, কিন্তু C compiler-গুলো এটি এড়িয়ে চলত — কারণটি পরের অংশের ইতিহাসে।
৬ · 8086-এর real mode — segment × 16 + offset
x86-এর segmentation-এর শুরু অনেক সরল, আর সুরক্ষার জন্য নয় — বড় মেমরিতে পৌঁছানোর জন্য। Intel 8086 (১৯৭৮) ছিল ১৬-bit প্রসেসর: এর register-এ সর্বোচ্চ ২১৬ = ৬৫,৫৩৬ পর্যন্ত সংখ্যা ধরে, অর্থাৎ ৬৪ KiB ঠিকানা। অথচ চিপে address line ছিল ২০টি — ২২০ = ১ MiB মেমরি। ১৬-bit register দিয়ে ২০-bit ঠিকানা বানাতে Intel একটি কৌশল নেয়: একটি segment register-এর মানকে ১৬ দিয়ে গুণ করে (অর্থাৎ বাঁয়ে ৪ bit সরিয়ে, hex-এ শেষে একটি 0 বসিয়ে) offset যোগ করা। এই মোডই আজ real mode নামে পরিচিত — পর্ব ৭-এ দেখেছেন, আজকের x86 PC-ও reset-এর পর এই মোডে জাগে।
0x1234:0x0010 → 0x12340 + 0x0010 = 0x12350
এখানে কোনো table নেই, limit-পরীক্ষা নেই, privilege নেই। প্রতিটি segment ঠিক ৬৪ KiB-এর একটি জানালা, যা মেমরির যেকোনো ১৬-বাইট সীমানায় (Intel-এর ভাষায় “paragraph”) শুরু হতে পারে। যেকোনো প্রোগ্রাম segment register-এ যেকোনো মান বসিয়ে পুরো ১ MiB পড়তে-লিখতে পারে — OS-এর মেমরিও। MS-DOS-এর যুগে একটি বাগওয়ালা প্রোগ্রাম পুরো কম্পিউটার আটকে দিতে পারত, এর কারণ এটাই।
1 0x1234:0x00102 0x1235:0x00003 0x1000:0x23504 0x07C0:0x0000 and 0x0000:0x7C005 0xFFFF:0x00106 0xFFFF:0xFFFF
| segment × 16 | |
| + offset | |
| যোগফল | |
| 8086 (A0–A19) | |
| 286+, A20 খোলা |
- মূল হিসাব।
0x1234-কে ১৬ দিয়ে গুণ = hex-এ শেষে একটি শূন্য:0x12340. তারপর offset0x0010যোগ:0x12350. দশমিকে ৭৪,৫৬০ + ১৬ = ৭৪,৫৭৬। - অন্য জোড়া, একই বাইট।
0x1235:0x0000→0x12350+ ০ = একই ঠিকানা। Segment এক বাড়ালে জানালা ১৬ বাইট সরে, তাই offset ১৬ কমালে একই জায়গায় পৌঁছানো যায়। - তৃতীয় জোড়া।
0x1000:0x2350→0x10000+0x2350=0x12350. আসলে এই বাইটে মোট ৪০৯৬টি ভিন্ন segment:offset জোড়া পৌঁছায় — simulator তা গুনে দেখাবে। এজন্য real mode-এ দুটি pointer-এর মান আলাদা হলেও তারা একই বাইট দেখাতে পারে; শুধু তুলনা করে বলা যায় না। - বুটের পরিচিত ঠিকানা। BIOS বুট সেক্টর লোড করে physical
0x7C00-এ (পর্ব ৭)। কেউ লেখেন0x07C0:0x0000, কেউ0x0000:0x7C00— দুটোই একই বাইট। কোন জোড়ায় লাফ দেওয়া হলো, তা bootloader-এর কোডে গুরুত্বপূর্ণ, কারণ পরের সব offset সেই segment ধরে হিসাব হয়। - ১ MiB-এর বাইরে।
0xFFFF:0x0010→0xFFFF0+0x10=0x100000— ২১-bit সংখ্যা! 8086-এ address line মাত্র ২০টি (A0–A19), তাই ২০ নম্বর bit কোথাও যায় না, ঠিকানা ঘুরে0x00000-এ ফেরে (wraparound). - সর্বোচ্চ মান।
0xFFFF:0xFFFF→0x10FFEF. 8086-এ তা হয়0x0FFEF; কিন্তু 80286 আর পরের চিপে address line বেশি, তাই A20 line চালু থাকলে সত্যিই ১ MiB-এর ওপরের ৬৫,৫২০ বাইট পৌঁছানো যায় (এই এলাকাকে পরে HMA বলা হয়)। একই প্রোগ্রাম দুই চিপে দুই জায়গায় যায় — এখান থেকেই A20-এর গল্প।
// 8086 real mode: physical = segment * 16 + offset, on 20 address lines.
const hex = (n, w) => "0x" + n.toString(16).toUpperCase().padStart(w, "0");
const pairs = [[0x1234, 0x0010], [0x1235, 0x0000], [0x1000, 0x2350], [0x1200, 0x0350],
[0x07C0, 0x0000], [0x0000, 0x7C00], [0xB800, 0x0000],
[0xFFFF, 0x0010], [0xFFFF, 0xFFFF]];
for (const [seg, off] of pairs) {
const raw = seg * 16 + off; // can reach 0x10FFEF (21 bits)
const on8086 = raw & 0xFFFFF; // 8086 has only A0..A19: wraps around
let line = hex(seg, 4) + ":" + hex(off, 4) + " -> " + hex(on8086, 5);
if (raw !== on8086) line += " (wrapped; with A20 on: " + hex(raw, 6) + ")";
console.log(line);
}
// How many segment:offset pairs reach the same physical byte 0x12350?
let count = 0, lo = null, hi = null;
for (let seg = 0; seg <= 0xFFFF; seg++) {
const off = 0x12350 - seg * 16;
if (off >= 0 && off <= 0xFFFF) { count++; if (lo === null) lo = seg; hi = seg; }
}
console.log("pairs reaching 0x12350: " + count + " (segment " + hex(lo, 4) + " .. " + hex(hi, 4) + ")");আউটপুট:
0x1234:0x0010 -> 0x12350
0x1235:0x0000 -> 0x12350
0x1000:0x2350 -> 0x12350
0x1200:0x0350 -> 0x12350
0x07C0:0x0000 -> 0x07C00
0x0000:0x7C00 -> 0x07C00
0xB800:0x0000 -> 0xB8000
0xFFFF:0x0010 -> 0x00000 (wrapped; with A20 on: 0x100000)
0xFFFF:0xFFFF -> 0x0FFEF (wrapped; with A20 on: 0x10FFEF)
pairs reaching 0x12350: 4096 (segment 0x0236 .. 0x1235)
প্রথম চার লাইন চারটি ভিন্ন জোড়া, একই physical বাইট 0x12350. 0xB800:0x0000 → 0xB8000 হলো PC-র রঙিন text-mode স্ক্রিনের মেমরি; DOS-যুগের প্রোগ্রাম সরাসরি এখানে লিখে অক্ষর দেখাত। শেষ দুই জোড়ায় যোগফল ২০ bit ছাড়ায়; & 0xFFFFF ঠিক 8086-এর মতো ওপরের bit ফেলে দেয়। শেষ লাইন: segment 0x0236 থেকে 0x1235 পর্যন্ত প্রতিটি segment-এর জন্য একটি বৈধ offset আছে — মোট ৪০৯৬টি জোড়া। কারণ offset ১৬-bit, অর্থাৎ ৬৫,৫৩৬টি মান, আর segment এক বাড়লে offset ১৬ কমে: ৬৫,৫৩৬ ÷ ১৬ = ৪০৯৬। (খুব নিচের ঠিকানায়, যেমন 0x00005-এ, জোড়া কম — কারণ segment ঋণাত্মক হতে পারে না।)
A20 — একটি পুরোনো আচরণকে বাঁচিয়ে রাখা
8086-এর wraparound-কে কিছু পুরোনো প্রোগ্রাম কাজে লাগিয়েছিল বলে প্রচলিত বিবরণে বলা হয় — যেমন 0xFFFF-এর কাছাকাছি segment দিয়ে মেমরির একদম শুরুর ডেটা পড়া। ১৯৮৪ সালের IBM PC/AT-এ 80286 এল, যার address line ২৪টি; এখন 0xFFFF:0x0010 সত্যিই 0x100000-এ যায়, ঘুরে আসে না। পুরোনো প্রোগ্রাম যাতে না ভাঙে, IBM মাদারবোর্ডে ২০ নম্বর address line (A20)-এর পথে একটি “গেট” বসায়, যা বন্ধ রাখলে bit 20 সবসময় শূন্য — অর্থাৎ 8086-এর মতো wraparound. গেটটি নিয়ন্ত্রণ করা হতো, অদ্ভুতভাবে, কীবোর্ড controller চিপের একটি বাড়তি pin দিয়ে; পরে দ্রুততর উপায়ও (যেমন একটি system I/O port) যোগ হয়। কয়েক দশক ধরে প্রতিটি bootloader আর OS-কে বুটের সময় A20 খোলার কোড রাখতে হয়েছে। নতুন প্রজন্মের কিছু CPU আর প্ল্যাটফর্মে এই সুবিধা সরিয়ে ফেলা হয়েছে বলে জানা যায় — বিস্তারিত নির্মাতাভেদে ভিন্ন।
DOS-যুগের দাম: real mode-এ একটি segment সর্বোচ্চ ৬৪ KiB, তাই এর চেয়ে বড় array রাখা ঝামেলা। DOS-এর C compiler-গুলোতে তাই “memory model” বেছে নিতে হতো (tiny, small, medium, compact, large, huge), আর pointer ছিল দুই ধরনের: শুধু offset-এর ১৬-bit near pointer আর segment:offset-এর ৩২-bit far pointer. ভুল ধরনের pointer মেশালে সূক্ষ্ম বাগ হতো। প্রোগ্রামাররা segmentation-কে সুবিধা নয়, বোঝা হিসেবে মনে রেখেছেন — এই অভিজ্ঞতাই পরে flat model-এর প্রতি টান তৈরি করে।
৭ · 80286 থেকে flat model, আর x86-64-এর FS/GS
80286 — প্রথম protected mode
Intel 80286 (১৯৮২) প্রথম আনে protected mode: segment register-এর মান আর সরাসরি ঠিকানা নয়, বরং একটি selector, যা descriptor table-এর entry দেখায় — অংশ ৫-এর কাঠামো। Descriptor-এ ২৪-bit base (১৬ MiB physical মেমরি), ১৬-bit limit (প্রতিটি segment তখনও সর্বোচ্চ ৬৪ KiB), অধিকার আর privilege level. এখন limit-পরীক্ষা হয়, আর একটি user প্রোগ্রাম kernel-এর segment ছুঁতে পারে না। কিন্তু 286-এ paging ছিল না, তাই virtual memory মানে পুরো segment ডিস্কে সরানো; আর protected mode থেকে real mode-এ ফেরার সহজ পথ ছিল না, যা DOS প্রোগ্রাম চালাতে বড় সমস্যা হয়েছিল।
80386 আর flat model
80386 base আর offset দুটোই ৩২-bit করে, granularity bit দিয়ে একটি segment-কে ৪ GiB পর্যন্ত বাড়ানোর সুযোগ দেয়, আর paging যোগ করে। এর ফলে একটি সরল কৌশল সম্ভব হলো: সব segment-এর base = 0 আর limit = ৪ GiB. তাহলে linear = 0 + offset = offset — segmentation কার্যত স্বচ্ছ হয়ে যায়, আর সব সুরক্ষা আর ব্যবস্থাপনা চলে paging দিয়ে। এটিই flat memory model. Linux, Windows NT-পরিবার আর অন্যান্য আধুনিক ৩২-bit OS মূলত এই পথ নিয়েছে: শুধু চারটি প্রধান descriptor — kernel code, kernel data, user code, user data — সবগুলোর base 0, পার্থক্য শুধু DPL (০ বনাম ৩) আর ধরনে। Segment তখন শুধু privilege level বহন করে: CS-এর নিচের দুই bit-ই এখনকার privilege, CPL — পর্ব ৪৮-এর ring ০ বনাম ring ৩।
x86-64-এর long mode — segmentation প্রায় বন্ধ
AMD-র ৬৪-bit সম্প্রসারণ (AMD64, পরে Intel-ও গ্রহণ করে; সাধারণ নাম x86-64) flat model-কে হার্ডওয়্যারের নিয়মে পরিণত করে। ৬৪-bit long mode-এ CS, DS, ES আর SS-এর base শূন্য ধরা হয়, আর limit-পরীক্ষা হয় না। CS-এর descriptor থেকে শুধু কয়েকটি তথ্য নেওয়া হয় — যেমন এটি ৬৪-bit code কি না, আর privilege level. কিন্তু দুটি ব্যতিক্রম রাখা হয়: FS আর GS-এর base এখনো যোগ হয়, আর তাদের ৬৪-bit base বসানো যায় বিশেষ register (MSR) দিয়ে। কেন? কারণ দুটি জিনিসের জন্য “একটি register যা প্রতিটি thread বা CPU-র নিজস্ব এলাকা দেখায়” খুব কাজের:
- User-space-এর thread-local storage: C-এর
__threadবা C11-এর_Thread_localভেরিয়েবলের প্রতিটি thread-এ আলাদা কপি থাকে (পর্ব ১৫)। Linux x86-64-এ প্রতিটি thread-এর FS base সেই thread-এর TLS এলাকা দেখায়, তাই%fs:-4-এর মতো একটি ঠিকানা প্রতিটি thread-এ আলাদা বাইটে যায় — অথচ নির্দেশ একই। Context switch-এর সময় kernel FS base বদলে দেয়। - Kernel-এর per-CPU ডেটা: Linux kernel x86-64-এ GS base দিয়ে বর্তমান CPU-র নিজস্ব ডেটা (যেমন এখন কোন task চলছে) খুঁজে পায়। User থেকে kernel-এ ঢোকার সময় SWAPGS নির্দেশ user-এর GS base আর kernel-এর GS base অদলবদল করে।
OS-ভেদে ব্যবহার আলাদা — এটি সার্বজনীন নিয়ম নয়: ৩২-bit Linux (i386) TLS-এর জন্য GS ব্যবহার করত; Windows x64-এ user-mode-এ GS প্রতিটি thread-এর TEB (Thread Environment Block) দেখায়, আর ৩২-bit Windows-এ সেই কাজ করত FS; macOS x86-64-এ thread-নির্দিষ্ট ডেটার জন্য GS ব্যবহারের কথা জানা যায়। ধারণা এক, register আলাদা।
প্রথমে দেখি একটি Linux x86-64 প্রসেসের segment register-এ আসলে কী আছে। Segment register পড়া সাধারণ (privileged নয়) নির্দেশ:
#include <stdio.h>
static void show(const char *name, unsigned short s) {
printf("%s = 0x%04x index %2u TI %u (%s) RPL %u\n", name, s,
s >> 3, (s >> 2) & 1, ((s >> 2) & 1) ? "LDT" : "GDT", s & 3);
}
int main(void) {
unsigned short cs, ss, ds, fs, gs;
__asm__ volatile ("mov %%cs, %0" : "=r"(cs)); /* read selectors: allowed */
__asm__ volatile ("mov %%ss, %0" : "=r"(ss)); /* in user mode */
__asm__ volatile ("mov %%ds, %0" : "=r"(ds));
__asm__ volatile ("mov %%fs, %0" : "=r"(fs));
__asm__ volatile ("mov %%gs, %0" : "=r"(gs));
show("CS", cs);
show("SS", ss);
show("DS", ds);
show("FS", fs);
show("GS", gs);
return 0;
}Wandbox-এর Linux x86-64 container-এ একটি রানের আউটপুট:
CS = 0x0033 index 6 TI 0 (GDT) RPL 3
SS = 0x002b index 5 TI 0 (GDT) RPL 3
DS = 0x0000 index 0 TI 0 (GDT) RPL 0
FS = 0x0000 index 0 TI 0 (GDT) RPL 0
GS = 0x0000 index 0 TI 0 (GDT) RPL 0
CS = 0x33: GDT-র ৬ নম্বর entry, RPL ৩ — user mode-এর ৬৪-bit code segment. SS = 0x2b: entry ৫, user data. এই দুটি মান Linux x86-64-এ স্থির, প্রতিটি user প্রসেসে একই। DS, FS, GS শূন্য — “null selector”, কারণ long mode-এ DS-এর দরকারই নেই, আর FS/GS-এর base selector দিয়ে নয়, MSR দিয়ে বসানো হয়। তাই FS = 0 দেখেও ভাববেন না TLS নেই — base অন্য জায়গায় থাকে, নিচের প্রোগ্রাম সেটি দেখাবে। অন্য OS বা virtual machine-এ (যেমন Windows-এ) selector-এর মান আলাদা হবে।
এবার thread-local ভেরিয়েবল। দুটি thread একই নামের tls_var-এ আলাদা মান লেখে; একটি barrier দুজনকে একসাথে বাঁচিয়ে রাখে, যাতে তুলনাটি অর্থপূর্ণ হয়। প্রতিটি thread arch_prctl(ARCH_GET_FS) system call দিয়ে নিজের FS base জেনে নেয় — এটি Linux x86-64-নির্দিষ্ট call, অন্য OS-এ নেই।
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <asm/prctl.h>
__thread int tls_var = 0; /* every thread gets its own copy */
pthread_barrier_t bar; /* keeps both threads alive together */
struct info { int value; void *addr; long off; };
static void *worker(void *arg) {
struct info *in = arg;
unsigned long fs = 0;
tls_var = in->value; /* write my own copy */
syscall(SYS_arch_prctl, ARCH_GET_FS, &fs); /* my FS base (TLS pointer) */
in->addr = &tls_var;
in->off = (long)((char *)&tls_var - (char *)fs);
pthread_barrier_wait(&bar); /* both threads exist now */
in->value = tls_var; /* still my value? */
return NULL;
}
int main(void) {
struct info a = {111, 0, 0}, b = {222, 0, 0};
pthread_t t1, t2;
pthread_barrier_init(&bar, NULL, 2);
pthread_create(&t1, NULL, worker, &a); pthread_create(&t2, NULL, worker, &b);
pthread_join(t1, NULL); pthread_join(t2, NULL);
printf("thread A sees tls_var = %d, thread B sees %d\n", a.value, b.value);
printf("main thread sees tls_var = %d\n", tls_var);
printf("&tls_var differs between A and B: %s\n", a.addr != b.addr ? "yes" : "no");
printf("offset from FS base: A %ld, B %ld -> same: %s\n",
a.off, b.off, a.off == b.off ? "yes" : "no");
return 0;
}আউটপুট (একটি রান; ঠিকানা প্রতিবার বদলায়, তাই প্রোগ্রাম শুধু সম্পর্ক ছাপে):
thread A sees tls_var = 111, thread B sees 222
main thread sees tls_var = 0
&tls_var differs between A and B: yes
offset from FS base: A -4, B -4 -> same: yes
এক নামের ভেরিয়েবল, তিন thread-এ তিন মান: A দেখে ১১১, B দেখে ২২২, main দেখে ০ — কেউ কারও মান মুছে দেয়নি। &tls_var দুই thread-এ আলাদা ঠিকানা। কিন্তু নিজের FS base থেকে দূরত্ব দুজনেরই ঠিক −৪: compiler tls_var-এর জন্য একটিই নির্দেশ তৈরি করেছে, মোটামুটি “%fs:-4 ঠিকানা থেকে পড়া”, আর প্রতিটি thread-এর আলাদা FS base সেই নির্দেশকে আলাদা জায়গায় পাঠায়। x86-64-এর TLS-বিন্যাসে ভেরিয়েবলগুলো thread pointer-এর নিচে থাকে, তাই দূরত্ব ঋণাত্মক; −৪ মানটি এই ছোট binary-র জন্য, অন্য প্রোগ্রামে আলাদা হতে পারে। পরীক্ষার সময় আমরা প্রথমে barrier ছাড়া — একটি thread join করে তারপর পরেরটি তৈরি করে — চালিয়েছিলাম; তখন দুই ঠিকানা একই এসেছিল, কারণ glibc শেষ হওয়া thread-এর stack আর TLS এলাকা নতুন thread-কে আবার দেয়। তাই “আলাদা ঠিকানা” শুধু একসাথে বেঁচে থাকা thread-এর বেলায় নিশ্চিত।
আরও গভীরে: Segment register-এর লুকানো descriptor cache-এর একটি বিখ্যাত পার্শ্বফল আছে। Protected mode-এ একটি segment-এ বড় limit বসিয়ে তারপর real mode-এ ফিরলে cache-এ সেই বড় limit থেকে যায়, তাই real mode-এর কোডও ১ MiB-এর ওপরে পৌঁছাতে পারে — অনানুষ্ঠানিক নাম “unreal mode”, firmware আর পুরোনো bootloader এটি কাজে লাগিয়েছে বলে জানা যায়। FS/GS base বদলাতে আগে kernel-এ system call লাগত; Intel Ivy Bridge প্রজন্ম থেকে FSGSBASE নির্দেশ (RDFSBASE, WRFSBASE…) আছে, আর Linux সেগুলো user-space-এ ব্যবহারের অনুমতি দেয় ৫.৯ সংস্করণ (২০২০) থেকে — দেরির একটি বড় কারণ ছিল নিরাপত্তা: kernel যেন কখনো user-এর বসানো GS base-কে নিজের per-CPU ঠিকানা ভেবে বিশ্বাস না করে। Intel ২০২৩ সালে x86S নামে একটি সরলীকৃত নকশা প্রস্তাব করেছিল, যেখানে real mode আর পুরোনো segmentation আরও ছেঁটে ফেলার কথা ছিল; পরে Intel প্রস্তাবটি থেকে সরে এসেছে বলে খবর প্রকাশিত হয়েছে।
৮ · ARM-এ segmentation নেই — কেন paging জিতল
ARM (ফোন, Apple silicon, Raspberry Pi, অনেক ক্লাউড সার্ভার) আর RISC-V-তে segmentation নেই-ই: ঠিকানা সরাসরি virtual, আর MMU শুধু page table দেখে। TLS-এর জন্য AArch64-এ আছে একটি সাধারণ system register — TPIDR_EL0 — যেখানে OS প্রতিটি thread-এর TLS ঠিকানা রাখে; x86-এর FS base-এর মতো একই কাজ, segmentation-এর কোনো যন্ত্রপাতি ছাড়া। RISC-V-তে একটি সাধারণ register (tp) এই কাজ করে। অর্থাৎ FS/GS-এর কাজটি দরকারি, কিন্তু তার জন্য segmentation দরকারি নয়।
তাহলে segmentation-এর এত সুন্দর ধারণা — অর্থপূর্ণ সুরক্ষা, সহজ sharing — কেন হারল? কয়েকটি কারণ একসাথে কাজ করেছে:
- External fragmentation: ভিন্ন মাপের টুকরো সামলানো কঠিন; compaction ধীর। Paging-এর সমান মাপের frame এই পুরো সমস্যাটি মুছে দেয় (অংশ ৪)।
- Disk-এ সরানো: Virtual memory-তে (পর্ব ৩৫) দরকার শুধু ব্যবহৃত অংশ মেমরিতে রাখা। একটি বড় segment-এর সামান্য অংশ ব্যবহার হলেও পুরোটা আনতে হয়; page-এ শুধু দরকারি ৪ KiB.
- প্রোগ্রামিং ভাষা: C আর পরের প্রায় সব ভাষা ধরে নেয় একটি pointer একটি সরল সংখ্যা, আর যেকোনো দুই pointer তুলনা বা বিয়োগ করা যায়। Segment-যুক্ত pointer (far pointer) এই ধারণা ভাঙে, compiler জটিল করে, আর কোড ধীর করে।
- Portability: Unix আর পরে Linux অনেক ধরনের CPU-তে চলতে চেয়েছে, যাদের বেশিরভাগে segmentation নেই। সবার জন্য একই নকশা — paging — সহজ।
- গতি: 386-এ একটি segment register বদলানো (descriptor পড়া আর পরীক্ষা) সাধারণ register বদলানোর চেয়ে অনেক ধীর; flat model এই খরচ এড়ায়।
- Paging নিজেই যথেষ্ট সুরক্ষা দিল: page-এ read/write bit, user/supervisor bit, আর পরে NX (no-execute) bit এসে “code লেখা যাবে না, data চালানো যাবে না” নিয়ম page স্তরেই সম্ভব হলো (পর্ব ৪৮)। Segmentation-এর সবচেয়ে বড় সুবিধাটি তখন আর একচেটিয়া রইল না।
তবু segmentation-এর আত্মা হারায়নি। Linux-এ cat /proc/self/maps চালালে যে তালিকা আসে — code, data, heap, stack, প্রতিটি library আলাদা লাইনে, প্রতিটির নিজের অধিকার (r-xp, rw-p) — সেটি আসলে প্রোগ্রামারের segment-ছবি, শুধু হার্ডওয়্যারে page দিয়ে বাস্তবায়িত। Kernel এদের বলে VMA (virtual memory area), পর্ব ৩৮-এ দেখবেন। আজকের OS যুক্তির স্তরে segment ব্যবহার করে, হার্ডওয়্যারের স্তরে page.
$ cat /proc/self/maps
5581d6a3c000-5581d6a3e000 r--p 00000000 08:01 1835 /usr/bin/cat
5581d6a3e000-5581d6a43000 r-xp 00002000 08:01 1835 /usr/bin/cat <- code
5581d6a47000-5581d6a48000 rw-p 0000a000 08:01 1835 /usr/bin/cat <- data
5581d7f1d000-5581d7f3e000 rw-p 00000000 00:00 0 [heap]
7f3b2c828000-7f3b2c9bd000 r-xp 00028000 08:01 2490 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffd5b4e1000-7ffd5b502000 rw-p 00000000 00:00 0 [stack]৯ · AI-এর যুগে segmentation আর x86-এর মেমরি
এই বিষয়ে AI-এর উত্তরে কয়েকটি ভুল বারবার দেখা যায় — কিছু হিসাবের, কিছু ইতিহাস আর বাস্তব সিস্টেমের:
| AI-এর ভুল | কেন ভুল | কীভাবে যাচাই করবেন |
|---|---|---|
| Trap-এর শর্ত “d > limit” লেখা | Limit = দৈর্ঘ্য; d = limit-ও বাইরে | ⟨১, ১৪⟩ (limit ১৪) দিয়ে উত্তর দেখুন |
| Real mode-এ “segment + offset × 16” | গুণ হয় segment-এ, offset-এ নয় | 0x07C0:0000 = 0x7C00 কি না মেলান |
| “x86-64-এ segmentation একেবারে নেই” | FS/GS base এখনো কাজ করে, CS privilege বহন করে | উপরের selectors.c আর tls-fs.c চালান |
| “Linux segmentation দিয়ে প্রসেস আলাদা রাখে” | Flat model; আলাদা রাখে paging | Kernel-এর GDT-তে base 0 দেখুন (kernel ডকুমেন্টেশন) |
| Linux আর Windows-এর FS/GS ব্যবহার গুলিয়ে ফেলা | OS আর ৩২/৬৪-bit ভেদে আলাদা | প্রশ্নে OS আর architecture স্পষ্ট লিখুন |
| ৬৪-bit কোডে ৩২-bit GDT বা real-mode উদাহরণ দেওয়া | Long mode-এ descriptor-এর অর্থ বদলে গেছে | Intel/AMD manual-এর সংশ্লিষ্ট অধ্যায় মিলিয়ে দেখুন |
অঙ্ক বা ব্যাখ্যা চাওয়ার সময় নিয়ম আর প্রেক্ষাপট স্পষ্ট লিখে দিন:
AI-এর উত্তর মানার আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Segmentation আর paging-এর মূল পার্থক্য? — Segment ভিন্ন মাপের, প্রোগ্রামারের কাছে দৃশ্যমান, external fragmentation আছে; page সমান মাপের, স্বচ্ছ, শুধু internal fragmentation.
- Base ২১৯, limit ৬০০ হলে ⟨০, ৪৩০⟩ আর ⟨০, ৬০০⟩? — ৬৪৯; আর ৬০০ ≥ ৬০০, তাই trap.
- STBR আর STLR কী? — Segment table মেমরির কোথায় আর কতটি segment; context switch-এ বদলায়।
- 8086-এ
0x1234:0x0010-এর physical address? —0x12340+0x0010=0x12350, ২০-bit. - আধুনিক Linux x86-64 segmentation ব্যবহার করে? — Flat model; সুরক্ষা paging-এ। শুধু FS (user TLS) আর GS (kernel per-CPU) base কাজে লাগে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১MULTICS-এর স্বপ্ন ছিল “সব কিছুই segment” — ফাইলও ঠিকানা-জগতের অংশ। এই নকশা কেন টিকল না, আর এর কোন ধারণাগুলো আজও প্রতিটি OS-এ বেঁচে আছে?
MULTICS-এর নকশাকাররা একটি সাহসী প্রশ্ন করেছিলেন: ফাইল আর মেমরি আলাদা কেন? একটি প্রোগ্রাম ফাইল “পড়ে” না — ফাইলটিকে একটি segment হিসেবে নিজের ঠিকানা-জগতে যুক্ত করে, তারপর সাধারণ pointer দিয়ে ব্যবহার করে। এই “single-level store” চিন্তায় buffer, read/write call, ফাইল-ফরম্যাট থেকে মেমরি-ফরম্যাটে রূপান্তর — সব অদৃশ্য হয়ে যায়। প্রতিটি segment-এর নিজের অধিকার আর protection ring থাকায় নিরাপত্তাও স্বাভাবিকভাবে আসত। Library-ও ছিল segment, প্রথমবার ব্যবহারের সময় চলার মধ্যেই যুক্ত হতো — আজকের dynamic linking-এর পূর্বসূরি।
কিন্তু বাস্তবতা কঠিন ছিল। হার্ডওয়্যার (GE-645) দামি আর জটিল; প্রকল্প সময়মতো শেষ হয়নি, আর ১৯৬৯ সালে Bell Labs সরে দাঁড়ায়। সেখান থেকেই Unix-এর জন্ম — Thompson আর Ritchie ইচ্ছা করে সরল পথ নেন: ফাইল হলো বাইটের সারি, পড়তে হয় read দিয়ে। Segment-এর মাপও সীমিত ছিল, তাই খুব বড় ফাইল একটি segment-এ আঁটত না। আর সবচেয়ে বড় কথা, এমন নকশা নির্দিষ্ট হার্ডওয়্যারের সাথে বাঁধা, যেখানে Unix-এর সরলতা তাকে প্রায় যেকোনো মেশিনে নিয়ে গেল। MULTICS নিজে কয়েক দশক কিছু প্রতিষ্ঠানে চলেছে, কিন্তু বাজার জিতেছে Unix-এর বংশধরেরা।
তবু MULTICS-এর ধারণাগুলো হারায়নি, শুধু অন্য পোশাকে ফিরেছে। mmap দিয়ে ফাইলকে ঠিকানা-জগতে আনা ঠিক MULTICS-এর চিন্তা, শুধু page দিয়ে বাস্তবায়িত (পর্ব ৩৮)। Protection ring x86-এর ring ০–৩ হয়ে প্রতিটি PC-তে আছে। Dynamic linking আর shared library আজ সর্বত্র। Hierarchical file system (directory-র গাছ) আর ব্যবহারকারী-ভিত্তিক access control-এর অনেক ধারণাও MULTICS থেকে Unix হয়ে এসেছে।
মূল উপলব্ধি: একটি নকশা বাজারে হেরে গেলেও তার ধারণা জিততে পারে — MULTICS-এর segment-স্বপ্ন আজ page-এর ওপর বসানো mmap, ring আর shared library হিসেবে বেঁচে আছে।
প্র ০২8086-এর segment × 16 + offset নকশা কেন বেছে নেওয়া হয়েছিল? একে কি “খারাপ নকশা” বলা ন্যায্য, নাকি সেই সময়ের সীমার মধ্যে একটি বুদ্ধিমান সমাধান?
১৯৭০-এর দশকের শেষে Intel-এর সামনে ছিল কয়েকটি বাস্তব সীমা। ৮-bit 8080 আর 8085-এর জন্য লেখা বিপুল সফটওয়্যার ছিল, আর Intel চেয়েছিল সেগুলো সহজে নতুন চিপে আনা যাক — 8080-এর ৬৪ KiB ঠিকানা-জগৎ একটি segment-এ বসে যায়, তাই পুরোনো প্রোগ্রাম অনুবাদ করে একটি segment-এ চালানো সহজ। আবার চিপের ট্রানজিস্টর-বাজেট সীমিত; ২০ বা ২৪-bit-এর পূর্ণ register আর ALU বানানো দামি। সমাধান: সব হিসাব ১৬-bit-এ, শুধু ঠিকানা বানানোর সময় একটি ৪-bit সরানো আর যোগ। চিপে যোগ করা হার্ডওয়্যার সামান্য, ফল ১ MiB — সেই সময়ের জন্য বিশাল।
এছাড়া এই নকশায় relocation সস্তা: একটি ছোট প্রোগ্রাম যদি শুধু একটি segment-এর ভেতরে থাকে, OS তাকে মেমরির যেকোনো ১৬-বাইট সীমানায় বসিয়ে শুধু segment register বদলে চালাতে পারে, কোডের ভেতরের ঠিকানা বদলাতে হয় না। DOS-এর .COM ফাইল ঠিক এভাবে চলত। Intel তখন জটিল, সুরক্ষাভিত্তিক একটি আর্কিটেকচার (iAPX 432) নিয়েও কাজ করছিল, আর 8086-কে অনেকে অন্তর্বর্তী চিপ হিসেবে দেখেছিলেন — কেউ ভাবেননি এটি কয়েক দশক টিকবে।
দাম এসেছে পরে। ১৯৮১ সালে IBM PC-তে 8088 (8086-এর ৮-bit bus-এর সংস্করণ) বসায়, আর PC-র বিস্ফোরণে এই নকশা চিরস্থায়ী হয়ে যায়। ৬৪ KiB-এর বড় array, near/far pointer, memory model, একই বাইটের হাজার রকম ঠিকানা, আর পরে A20 — সবই এই সিদ্ধান্তের ফল। একটি অন্তর্বর্তী সমাধান সামঞ্জস্যের চাপে ইতিহাসের সবচেয়ে বেশি ব্যবহৃত আর্কিটেকচারের ভিত্তি হয়ে গেল।
তাই “খারাপ নকশা” রায়টি অন্যায্য; বরং এটি তার সময়ের সীমার মধ্যে চতুর, কিন্তু দীর্ঘ জীবনের জন্য নকশা করা হয়নি। প্রকৌশলের শিক্ষাটি হলো — কোনো ইন্টারফেস একবার বহু মানুষ ব্যবহার করতে শুরু করলে, তার অস্থায়ী সিদ্ধান্তগুলোও স্থায়ী হয়ে যায়।
মূল উপলব্ধি: নকশার মান বিচার করতে হয় তার সময়ের সীমা দিয়ে; কিন্তু সফল নকশা প্রায়ই তার পরিকল্পিত আয়ুর চেয়ে অনেক বেশি দিন বাঁচে, আর তখন তার শর্টকাটগুলোর দাম দিতে হয় সবাইকে।
প্র ০৩A20 gate-এর গল্প দেখায় একটি পুরোনো আচরণকে বাঁচাতে কয়েক দশক ধরে বাড়তি হার্ডওয়্যার আর কোড রাখা হয়েছে। Backward compatibility-র এই দাম কি যুক্তিসঙ্গত ছিল?
প্রথমে ঘটনাটি সংক্ষেপে। 8086-এ ২০টি address line, তাই ১ MiB-এর ওপরের ঠিকানা ঘুরে শুরুতে আসত। প্রচলিত বিবরণ অনুযায়ী কিছু প্রোগ্রাম — DOS-এর কিছু অংশসহ বলে প্রায়ই উল্লেখ করা হয় — এই আচরণের ওপর নির্ভর করত। 80286-এ address line বাড়ল, wraparound বন্ধ হলো, আর সেই প্রোগ্রামগুলো ভাঙার ঝুঁকি দেখা দিল। IBM PC/AT-এ সমাধান: A20 line-এ একটি গেট, যা ডিফল্টভাবে বন্ধ। গেট খুলতে কীবোর্ড controller-কে নির্দেশ পাঠাতে হতো — ধীর আর অদ্ভুত, কিন্তু সেই সময়ে বোর্ডে বাড়তি pin সেখানেই ছিল। পরে Windows, Linux আর প্রতিটি bootloader-কে বুটের শুরুতে A20 খোলার কোড রাখতে হয়েছে, আর কিছু ভিন্ন পদ্ধতিও এসেছে কারণ সব বোর্ডে একটি পদ্ধতি কাজ করত না।
এই দাম কি যুক্তিসঙ্গত? তখনকার বাজার দেখলে উত্তর সম্ভবত “হ্যাঁ”। IBM PC-র সাফল্যের মূল কারণ ছিল বিপুল সফটওয়্যার; নতুন মেশিনে পুরোনো জনপ্রিয় প্রোগ্রাম না চললে গ্রাহক কিনতেন না। একটি ছোট গেটের দাম খুব কম, আর তা রাখতে বাজার হারানোর ঝুঁকি নেই। সমস্যা হলো দাম জমে — প্রতিটি নতুন প্রজন্ম আগেরটির সামঞ্জস্য রাখে, তাই ১৯৮৪-র সিদ্ধান্ত কয়েক দশক পরের চিপেও বয়ে বেড়াতে হয়েছে, যদিও সেই পুরোনো প্রোগ্রাম চালানো লোক নগণ্য। প্রতিটি OS-ডেভেলপারকে এই ইতিহাস শিখতে হয়, আর প্রতিটি বুট কোডে কয়েকটি বাড়তি, ভুল-প্রবণ লাইন।
তুলনা করুন Apple-এর পথ: তারা কয়েকবার পুরো CPU আর্কিটেকচার বদলেছে (68k থেকে PowerPC, তারপর Intel, তারপর নিজের ARM চিপ), প্রতিবার একটি অনুবাদক (emulator বা Rosetta) দিয়ে সাময়িক সামঞ্জস্য রেখে, তারপর পুরোনোকে বাদ দিয়ে। এটি সম্ভব হয়েছে কারণ Apple হার্ডওয়্যার আর OS দুটোই নিয়ন্ত্রণ করে। PC-জগতে হাজার নির্মাতা, কেউ একা পুরোনোকে বাদ দেওয়ার সাহস পায় না। UEFI-তে যাওয়া আর CSM (পর্ব ৭) ধীরে ধীরে বাদ পড়া এই ঋণ শোধের একটি দীর্ঘ প্রক্রিয়া।
মূল উপলব্ধি: সামঞ্জস্য স্বল্পমেয়াদে সস্তা, দীর্ঘমেয়াদে সুদসহ ঋণ; কে সেই ঋণ শোধের সিদ্ধান্ত নিতে পারে, তা নির্ভর করে পুরো ব্যবস্থাটি কতটা এক হাতে নিয়ন্ত্রিত।
প্র ০৪Paging জিতেছে, কিন্তু segmentation-এর “বাইট-নিখুঁত সীমানা”-র সুরক্ষা হারিয়ে গেছে। আজকের নিরাপত্তা-গবেষণায় এই ধারণা কি ফিরে আসছে?
Segmentation-এর একটি সত্যিকারের সুবিধা ছিল: একটি array-কে নিজের segment দিলে তার সীমার বাইরে লেখার চেষ্টা হার্ডওয়্যারই ধরে। Paging-এ সুরক্ষার একক ৪ KiB, তাই একটি ১০০ বাইটের buffer-এর পরের বাইটে লেখা — buffer overflow — হার্ডওয়্যার দেখে না, কারণ পাশের বাইটও একই page-এ, একই অধিকারে। C আর C++-এর বহু নিরাপত্তা-দুর্বলতার মূলে এই ধরনের সীমা-লঙ্ঘন। তাই সফটওয়্যার স্তরে নানা প্রতিরক্ষা এসেছে: stack canary, ASLR, guard page, AddressSanitizer-এর মতো পরীক্ষার টুল — প্রতিটি আংশিক।
হার্ডওয়্যার স্তরে ধারণাটি নতুন রূপে ফিরছে। Intel ২০১০-এর দশকে MPX (Memory Protection Extensions) এনেছিল — pointer-এর সাথে নিম্ন আর ঊর্ধ্ব সীমা রেখে পরীক্ষা, অনেকটা segment-এর limit-এর মতো; কিন্তু খরচ আর জটিলতার কারণে এটি জনপ্রিয় হয়নি, আর compiler ও Linux পরে এর সমর্থন সরিয়ে নেয়। এর চেয়ে আশাব্যঞ্জক একটি গবেষণা-ধারা হলো capability hardware — যেমন Cambridge বিশ্ববিদ্যালয় আর SRI-র CHERI প্রকল্প, যেখানে প্রতিটি pointer নিজেই একটি “capability”: ঠিকানার সাথে base, দৈর্ঘ্য আর অধিকার, হার্ডওয়্যার-রক্ষিত, জাল করা যায় না। এটি আসলে প্রতিটি pointer-কে একটি ছোট segment বানানো। ARM-এর সাথে মিলে Morello নামে একটি পরীক্ষামূলক বোর্ডও তৈরি হয়েছে বলে জানা যায়। আরেক দিকে ARM-এর Memory Tagging Extension (MTE) মেমরির ছোট টুকরো আর pointer-এ মিলিয়ে দেখার মতো tag রাখে — সীমা-লঙ্ঘন সম্ভাব্যভাবে ধরে।
লক্ষণীয়, এই নতুন নকশাগুলো segmentation-এর পুরোনো ভুলগুলো এড়াতে চায়: এরা paging বাদ দেয় না (fragmentation-এর সমস্যা ফেরে না), আর pointer-কে প্রোগ্রামারের কাছে আলাদা segment-নম্বর হিসেবে দেখায় না — compiler স্বয়ংক্রিয়ভাবে সীমা বসায়। অর্থাৎ ধারণাটি “ঠিকানা-ব্যবস্থাপনা” থেকে সরে “pointer-এর নিরাপত্তা”-য় গেছে।
মূল উপলব্ধি: Segmentation মেমরি-ব্যবস্থাপনার পদ্ধতি হিসেবে হেরেছে, কিন্তু তার সুরক্ষার ধারণা — প্রতিটি বস্তুর নিজের সীমা — capability আর memory tagging-এর রূপে নতুন করে পরীক্ষিত হচ্ছে।
প্র ০৫x86-64 যখন প্রায় সব segmentation বাদ দিল, তখন শুধু FS আর GS কেন রাখা হলো? ARM এই একই সমস্যা কীভাবে সমাধান করে, আর কোন পথটি ভালো?
সমস্যাটি এই: একটি multithreaded প্রোগ্রামে প্রতিটি thread-এর কিছু নিজস্ব ডেটা দরকার — errno-এর মতো ভেরিয়েবল, memory allocator-এর thread-নিজস্ব cache, আরও অনেক। কোডটি সব thread-এ এক, তাই কোডের ভেতরে স্থির ঠিকানা লেখা যায় না। দরকার একটি register, যা প্রতিটি thread-এ আলাদা এলাকা দেখায়, আর context switch-এ বদলায়। ৩২-bit x86-এ সাধারণ register মাত্র আটটি, একটিকে স্থায়ীভাবে এই কাজে আটকানো দামি। কিন্তু FS আর GS প্রায় অব্যবহৃত ছিল, আর তাদের segment base ঠিক এই কাজটিই করে — %fs:offset মানে “এই thread-এর এলাকা + offset”, বাড়তি নির্দেশ ছাড়া। তাই ৩২-bit Linux আর Windows এগুলোকে TLS আর thread block-এর জন্য ব্যবহার শুরু করে।
AMD যখন ২০০০ সালের দিকে ৬৪-bit সম্প্রসারণ নকশা করছিল, তখন এই ব্যবহার এত গভীরে চলে গিয়েছিল যে FS/GS বাদ দিলে সব OS-এর TLS আর kernel-এর per-CPU কোড নতুন করে লিখতে হতো। তাই AMD বাকি segmentation বন্ধ করে, শুধু FS আর GS-এর base রাখে, আর ৬৪-bit base বসানোর জন্য MSR দেয়; kernel-এর জন্য SWAPGS যোগ করে, যাতে user-এর GS আর kernel-এর GS দ্রুত অদলবদল হয়। এটি একটি ব্যবহারিক আপস: পুরোনো যন্ত্রের ছোট অংশ রেখে বাস্তব প্রয়োজন মেটানো।
ARM-এর AArch64 শুরু থেকে নকশা করা, কোনো segmentation-ঐতিহ্য ছাড়া। সেখানে TPIDR_EL0 নামে একটি system register শুধু এই কাজের জন্য — OS প্রতিটি thread-এর TLS ঠিকানা এতে রাখে, প্রোগ্রাম একটি নির্দেশে পড়ে, তারপর সাধারণ যোগ করে। Kernel-এর নিজস্ব per-CPU ঠিকানার জন্য আলাদা register আছে। এটি পরিচ্ছন্ন: বিশেষ ঠিকানা-গণনা নেই, SWAPGS-এর মতো ভুল-প্রবণ নির্দেশ নেই। SWAPGS-কে ঘিরে x86-এ কিছু সূক্ষ্ম নিরাপত্তা-সমস্যা হয়েছে — যেমন ২০১৯ সালে speculative execution-সংক্রান্ত একটি দুর্বলতা, যার প্রতিরক্ষা হিসেবে kernel-এ বাড়তি barrier যোগ করা হয়েছে। x86-এর পথের সুবিধা শুধু একটি: একই নির্দেশে TLS-এর ভেতরের ঠিকানায় সরাসরি পৌঁছানো, আলাদা যোগ ছাড়া — যা একসময় গুরুত্বপূর্ণ ছিল, আজ সামান্য।
মূল উপলব্ধি: “প্রতিটি thread বা CPU-র নিজস্ব এলাকা দেখানো একটি register” — চাহিদাটি সার্বজনীন; x86 পুরোনো segment-যন্ত্র পুনর্ব্যবহার করে তা মেটায়, ARM একটি সরল নিবেদিত register দিয়ে — নতুন নকশার বিলাসিতা।
অনুশীলন
হাতে হিসাব, আউটপুট অনুমান, কোড আর ব্যাখ্যা — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।
-
হাতে হিসাব: একটি প্রসেসের segment table — segment ০ (base ১৪০০, limit ১০০০), ১ (base ৬৩০০, limit ৪০০), ২ (base ৪৩০০, limit ৪০০), ৩ (base ৩২০০, limit ১১০০), ৪ (base ৪৭০০, limit ১০০০)। (ক) ⟨২, ৫৩⟩, ⟨৩, ৮৫২⟩, ⟨০, ১২২২⟩, ⟨১, ৩৯৯⟩, ⟨৪, ১০০০⟩-এর physical address কত, বা trap? (খ) ১৪০০ থেকে ৬৬৯৯-এর মধ্যে কোন কোন physical অংশ খালি, আর ৯০০ বাইটের একটি নতুন segment কি কোথাও আঁটবে?
(ক) ⟨২, ৫৩⟩: ৫৩ < ৪০০ → ৪৩০০ + ৫৩ = ৪৩৫৩. ⟨৩, ৮৫২⟩: ৮৫২ < ১১০০ → ৩২০০ + ৮৫২ = ৪০৫২. ⟨০, ১২২২⟩: ১২২২ ≥ ১০০০ → trap. ⟨১, ৩৯৯⟩: ৩৯৯ < ৪০০, শেষ বৈধ বাইট → ৬৩০০ + ৩৯৯ = ৬৬৯৯. ⟨৪, ১০০০⟩: ১০০০ ≥ ১০০০ → trap (ঠিক-সীমার ফাঁদ)।
(খ) দখল: ১৪০০–২৩৯৯ (seg ০), ৩২০০–৪২৯৯ (seg ৩), ৪৩০০–৪৬৯৯ (seg ২), ৪৭০০–৫৬৯৯ (seg ৪), ৬৩০০–৬৬৯৯ (seg ১)। খালি: ২৪০০–৩১৯৯ (৮০০ বাইট) আর ৫৭০০–৬২৯৯ (৬০০ বাইট)। মোট ১৪০০ বাইট খালি, কিন্তু সবচেয়ে বড় গর্ত ৮০০ — তাই ৯০০ বাইটের segment আঁটবে না, compaction ছাড়া। এটাই external fragmentation. (Segment-table simulator-এ
tableবদলে (ক) যাচাই করতে পারেন।) -
আউটপুট অনুমান: Real-mode simulator-এর
pairs-এ তিনটি জোড়া যোগ করা হলো —[0xFFFF, 0x0000],[0xA000, 0xFFFF]আর[0x0000, 0x0005]. এদের লাইন কী ছাপবে? আর শেষ লুপে0x12350-এর বদলে0x00005দিলে কয়টি জোড়া পাওয়া যাবে?0xFFFF:0x0000 -> 0xFFFF0— ১ MiB-এর শেষ ১৬ বাইটের শুরু; 8086 reset-এর পর ঠিক এখান থেকে প্রথম নির্দেশ পড়ত (CS =0xFFFF, IP = 0).0xA000:0xFFFF -> 0xAFFFF—0xA0000+0xFFFF, VGA গ্রাফিক্স মেমরির এলাকা।0x0000:0x0005 -> 0x00005. কোনোটিই ২০ bit ছাড়ায় না, তাই “wrapped” অংশ নেই। শেষ লুপে0x00005-এর জন্য মাত্র ১টি জোড়া: segment ০, offset ৫। Segment ১ নিলেই0x10> ৫, offset ঋণাত্মক হতো। অর্থাৎ “৪০৯৬ জোড়া” নিয়মটি শুধু0xFFF0থেকে0xFFFFFপর্যন্ত ঠিকানায় খাটে (A20 খোলা অবস্থায় ১ MiB-এর ওপরের HMA-তে segment-এর সর্বোচ্চ মান0xFFFF-এর কারণে জোড়া আবার কমে)। -
কোড লিখুন: একটি ছোট JavaScript function
decode(sel)লিখুন, যা একটি x86 selector নিয়ে index, TI (GDT/LDT) আর RPL ছাপে।0x0010,0x002B,0x0073আর0x000Fদিয়ে চালান — উত্তর আগে হাতে বের করুন।Selector-এর নিচের ২ bit RPL, তার ওপরের ১ bit TI, বাকিটা index:
function decode(sel) { const index = sel >> 3, ti = (sel >> 2) & 1, rpl = sel & 3; console.log("0x" + sel.toString(16).padStart(4, "0") + ": index " + index + ", " + (ti ? "LDT" : "GDT") + ", RPL " + rpl); } [0x0010, 0x002B, 0x0073, 0x000F].forEach(decode);ফল:
0x0010→ index ২, GDT, RPL ০ (৩৮৬ stepper-এর DS).0x002b→ index ৫, GDT, RPL ৩ (Linux x86-64-এর user SS).0x0073→ index ১৪, GDT, RPL ৩ (৩২-bit Linux-এর user code segment বলে পরিচিত)।0x000f→ index ১, LDT, RPL ৩ — TI bit = ১, তাই প্রসেসের নিজের LDT-র entry. -
ব্যাখ্যা করুন: tls-fs.c-কে বদলে barrier সরিয়ে দেওয়া হলো, আর thread দুটি পরপর চালানো হলো: প্রথমটি তৈরি করে join, তারপর দ্বিতীয়টি তৈরি করে join. একটি রানে “
&tls_var differs between A and B: no” ছাপা হলো। এটি কি TLS-এর বাগ? A আর B কি তাহলে একই ভেরিয়েবল ভাগ করছে?না, বাগ নয়। পরপর চালালে A শেষ হয়ে join হওয়ার পর তার stack আর TLS এলাকা মুক্ত হয়; glibc সেগুলো cache করে রাখে, আর B তৈরির সময় সেই একই এলাকা আবার দেয়। তাই B-এর FS base আর A-এর পুরোনো FS base এক, ফলে
&tls_var-ও এক। কিন্তু কোনো মুহূর্তে দুজন একসাথে বেঁচে ছিল না, তাই কেউ কারও মান দেখেনি — B শুরুতে tls_var-এর প্রাথমিক মান পায় (glibc নতুন thread-এর TLS আবার initialize করে), A-র ১১১ নয়। একই ঘটনা মেমরির অন্য জায়গাতেও দেখা যায়:freeকরা মেমরি পরেরmalloc-এ ফিরে আসে। শিক্ষা: “দুই thread-এর TLS ঠিকানা আলাদা” কথাটি একসাথে বেঁচে থাকা thread-এর জন্য নিশ্চিত; পরীক্ষার কোড লেখার সময় তাই barrier বা অন্য কোনো উপায়ে দুজনকে একসাথে রাখতে হয়। আমাদের প্রথম পরীক্ষামূলক রানে ঠিক এই “no” এসেছিল।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমIntel 386-এর protected mode-এ (paging চালু) একটি মেমরি-পড়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- নির্দেশ একটি offset দেয়, আর একটি segment register (যেমন DS) বেছে নেওয়া হয়
- Selector-এর index আর TI দিয়ে GDT বা LDT-র descriptor পাওয়া যায়
- Descriptor-এর limit আর অধিকার দিয়ে offset পরীক্ষা করা হয়
- Base + offset = linear address
- Linear address page directory আর page table দিয়ে অনুবাদ হয়
- Frame + page offset = physical address
একটি segment table-এ segment ২-এর base = ৯০ আর limit = ১০০। Logical address ⟨২, ৯৯⟩-এর ফল কী?
- Physical address ১৮৯
- Physical address ১৯০
- Trap, কারণ offset limit-এর খুব কাছে
- Physical address ৯৯
8086-এর real mode-এ segment = 0x2000, offset = 0x0100 হলে physical address কত?
0x21000x201000x2001000x03000
শুধু segmentation (paging ছাড়া) ব্যবহার করলে কোন সমস্যাটি সবচেয়ে বড় হয়ে ফেরে, যা paging দূর করেছিল?
- Internal fragmentation
- খুব বড় table
- External fragmentation
- Protection দেওয়া অসম্ভব হয়ে যায়
x86 selector-এর মান 0x002B হলে index, table আর RPL কী?
- index ৪৩, GDT, RPL ০
- index ৫, LDT, RPL ৩
- index ২, GDT, RPL ১
- index ৫, GDT, RPL ৩
Linux x86-64-এ একটি user প্রোগ্রামের FS segment register-এর base সাধারণত কী দেখায়?
- প্রোগ্রামের code segment-এর শুরু
- বর্তমান thread-এর thread-local storage এলাকা
- Kernel-এর per-CPU ডেটা
- GDT-র শুরু
Segmentation-এ দুটি প্রসেস একটি text editor-এর code ভাগ করতে চায়। সবচেয়ে সরল ব্যবস্থা কোনটি?
- দুই প্রসেসের segment table-এর code entry-তে একই base আর limit, অধিকার শুধু পড়া + চালানো
- প্রতিটি প্রসেসের জন্য code-এর আলাদা কপি, একই limit-সহ
- Code segment-এ পড়া-লেখা দুই অধিকার দেওয়া, যাতে দুজনেই আপডেট করতে পারে
- দুই প্রসেসের STBR-এ একই মান বসানো
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Segmentation: প্রোগ্রামারের চোখের ছবি — code, data, stack, heap, library আলাদা segment; logical address = ⟨s, d⟩। প্রতিটি segment-এর base আর limit থাকে segment table-এ, যা STBR আর STLR দিয়ে পাওয়া যায়।
- অনুবাদ: s < STLR, তারপর d < limit (ঠিক সমান হলেও trap), তারপর physical = base + d. পাঠ্যবইয়ের উদাহরণ: ⟨০, ৪৩০⟩ → ৬৪৯, ⟨১, ১০⟩ → ২৩১০, ⟨২, ৫০০⟩ trap, ⟨৩, ৪০০⟩ → ১৭২৭, ⟨৪, ১১২⟩ trap.
- লাভ ও দাম: segment-ভিত্তিক protection আর সহজ sharing; কিন্তু ভিন্ন মাপের অবিচ্ছিন্ন টুকরো বলে external fragmentation, compaction লাগে। MULTICS আর 386 segmentation-এর ওপর paging বসিয়ে দুটোর সুবিধা নিতে চেয়েছিল।
- x86-এর পথ: 8086 real mode — segment × 16 + offset, ২০-bit, wraparound আর A20; 80286 — descriptor, limit, privilege; 80386 — selector → descriptor → linear → paging → physical; আধুনিক OS — base 0-এর flat model.
- আজ: x86-64 long mode-এ শুধু FS/GS base কাজ করে — Linux-এ user TLS (%fs) আর kernel per-CPU (%gs). ARM-এ segmentation নেই, TLS-এর জন্য TPIDR_EL0. Paging জিতেছে, কিন্তু segment-এর চিন্তা VMA, mmap আর capability-গবেষণায় বেঁচে আছে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৫ · Virtual memory — demand paging, page fault আর copy-on-writeপরবর্তী পর্বশুধু দরকারি page মেমরিতে — page fault-এর পুরো পথ।
- অপারেটিং সিস্টেম · কোর্স হোম৬৪টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ৩২ · Paging — page, frame আর page tableপেছনে ফিরেযে পদ্ধতির সাথে segmentation-এর তুলনা।
- পর্ব ৭ · বুট প্রক্রিয়া — BIOS/UEFI, bootloader, kernel আর initreal modeReset-এর পর real mode, তারপর protected আর long mode-এ যাত্রা।
- C প্রোগ্রামিং · Pointer-এর ভিত্তিCFlat ঠিকানা-জগতে pointer আসলে কী — segmentation কেন C-এর সাথে মেলেনি তা বুঝতে।
- কম্পিউটার ফান্ডামেন্টালস · সংখ্যা পদ্ধতিhex ও বাইনারিSelector ভাঙা আর segment × 16-এর হিসাবের জন্য hex অনুশীলন।