Пост 3 тусдаа постноос бүрдэнэ
Linking гэдэг үгийг орчуулахгүй, шууд ашиглаад явна. Мөн програмчлалын С хэлийг ашиглах болно
Linking бол үнэндээ програмыг яг ажилдаг програм болгодог үйлдэл, operating system нь үүнийг хийж өгдөг. Compiler бол тухайн програмчлалын хэл дээр бичигдсэн кодыг, source code, шалгаж, сайжруулаад тухайн компьютер, машин таниж ажиллуулж чадах үйлдлүүдийн дараалал болгоод, machine code, машин код бүтэцтэй буюу хоёртын тоололд хөрвүүлэгч нь юм, object file гэж нэрлэдэг. Харин linking нь шууд санах ой буюу RAM рүү хуулж дангаараа ажиллах чадамжтай файл буюу програм үүсгэдэг, үүнийг executable object file гэдэг. Энэ бол компьютерын програм. Windows дээр бол .exe файл. Loader гэж тухайн executable object file--г санах ойруу дамжуулж тухайн програмд өөрт нь бүх эрхийг өгч ажиллах боломжийг нь олгодог үйлдлийн системийн програм. Доод түвшиндөө ямар нэгэн програм ажиллаж байхад, үйлдлийн систем ажиллахгүй, үйлдлийн систем ажиллаж байхад ямар ч програм ажиллахгүй. Ийм л зүйл байдаг, үүнийг бүр нарийн судалж үзье гэвэл үйлдлийн системийн онолыг судлаарай. Loader--г тайлбарлахдаа “бүрэн эрхийг өгч” гэдэг үгийг ашигласан нь ийм л учиртай юм. За одоо дээрх ойлголтуудаа ашиглан арай дээр байдлаар тайлбарлая.
Linking бол хэд хэдэн код, мөн дата--нуудын хамт нийлүүлээд нэг executable object file үүсгэх үйлдэл, үүсгэдэг үйлдлийн системийн програм. Linking--г дараах үед ашиглаж болно:
програмыг компайлдаж байх үед, тайлбарлавал, source code--г machine code болгон хөврүүлэх үед linking ашиглаж өөр source code, data--г холбож, нэгтгэж болно гэсэн үг.
Loader ажиллаж байх үед буюу санах ойруу хуулаад, loader ажиллаж байх үед нь мөн linking ашиглаж болно
Тухайн програм ажиллаж байх үед ч хүртэл linking ашиглаж болно
Програм хангамж хөгжүүлэлт linking маш чухал үүрэг гүйцэтгэдэг. Нэг дор маш том source file бичиж байснаас хэрэгтэй, хэрэгтэй модулиар нь салгаж тус тусад нь source file үүсгэж компайлдаад linking ашиглаж үндсэн програмаа гаргаж авна. Аль нэг модульд нь шинэчлэл, засвар хийвэл тухайн модулиа компайлдаад дахин linking ашиглаж холбоод л болоо. Модулиудад хуваан хэсэгчилэх санааг компьютерын шинжлэх ухааны аль ч хэсэг, салбарт ашигладаг. Monolithic VS micro-service/kernel.
Reference гэж, програм бичихэд ашиглагдсан функцүүдийн нэр, хувьсагчуудын нэрнүүдийг хэлнэ шүү, цаашдаа шууд reference гээд явна.
Reference resolve гэж тухайн reference--ийн ард ажиллах код, утга, дата--г нь олохыг хэлнэ
Linking--г бүрэн сайн ойлгосноор
Том хэмжээтэй, олон модулиас бүрдсэн програмыг link хийхэд ихэвчлэн ‘missing modules’, ‘missing libraries’, ‘incompatible library versions’ гэх алдаанууд гардаг. Линк хийхэд reference--үүдийн тодорхойлогч буюу яг цаана нь байгаа утга, функц болон функцын ажиллах кодыг, хэрхэн хайж олж холбодогийг ойлгохгүй бол энэ алдаа гарсаар л байх болно.
Linux--н линкерүүд ямар алгоритмаар reference resolve хийдэгийг мэдэхгүйгээс аюултай алдаа, bug--ууд гардаг. Тэр нь мэдэгдэхгүй, ямар ч warning байхгүй явчихдаг. Жишээ нь ижил нэртэй reference олдох үед ямар алгоритмаар resolve хийдэгийг мэдэхгүй л бол, энэнээс болоод програм чинь шаал өөрөөр ажичихдаг, буруу код ажилцан явж байдаг. Энэ ийм ажиллах үеийн буруу ажиллагаанууд debug хийхэд маш төвөгтэй
За шууд эхлэе
Жишээ програм:
main.c | sum.c |
int sum(int *a, int n); int array[2] = {1, 2}; int main() { int val = sum(array, 2); return val; } | int sum(int *a, int n) { int i, s = 0; for (i = 0; i < n; i++) { s += a[i]; } return s; } |
main.c дээр 2 урттай массив дата--тай үүсгээд, sum.c дээр тодорхойлогдсон sum функцыг ашиглаж байна.
Дээрх жишээ програмыг линкиг--г тайлбарлахдаа ашиглаад явна аа.
Бид компайлер гэж ярьдаг боловч үнэндээ Compiler Driver нь яг бүтэн том зурагаар нь хөрвүүлэгч програмыг хэлдэг. Компайлер нь Compiler Driver--н нэг алхам нь юм. Гэхдээ энэ тэгээд холиод л хэрэглэдэглдээ.
Compiler Driver
Ихэнх компайлерууд дараах алхамуудыг хийдэг. Preprocessor, compiler, assembler and linker. Дээрх кодуудаа одоо компайлдаж үзье.
shell>> gcc -Og -o prog main.c sum.c
За дээрх коммандын ард дараах зүйлс болж байгаа.
shell>> cpp [args] main.c /tmp/main.i
C PreProcessor ажилласан ба үүрэг нь, файл доторх include хийсэн сангууд олж авчирч, бас # directive--г тодорхойлогчуудыг тавьж main.i файлыг үүсгэдэг.
shell>> cc1 /tmp/main.i -Og [args] -o /tmp/main.s
Одоо С компайлер ажиллаж ассембли хэлрүү хөрвүүлж main.s файл үүсгэнэ. 1 ассембли мөр нь CPU 1 instructor байдаг. -О флаг бол компайлерын сайжруулах түвшин. -O1, -O2, -O3 гээд тоо нь ихсэх тусам илүү сайжруулж хурдтай програм үүсгэдэг. Гэхдээ debug хийхэд улам төвөгтэй болдог, мөн алдаа гарах магадлал улам ихсэнэ. -o флаг бол үүсэх файлыг хаана юу гэж нэрлэхэд л ашигладаг.
shel>> as [args] -o /tmp/main.o /tmp/main.s
as ассэмблер ажиллаж ассемблэр кодоос object file үүсгэдэг. Sum.c файлын хувьд яг адилан процессууд явагдаад хамгийн сүүлд манай линкер орж ирнэ.
shel>> ld -o prg [args] /tmp/main.o /tmp/sum.o
Одоо prg гэсэн executable object file үүснэ. ./prog гээд л шууд ажилуулахад үйлдлийн системийн loader гэсэн програмыг дуудааж ажилыг хийлгэнэ.
Object File
Гурван төрлийн object file байдаг
Relocatable object file: Энэ нь компайлдсан хоёртын кодоороо, дата--гаа агуулсан мөн мөн өөр relocatable object file--тай хамт компайлдах үед холбож шинэ executable object file үүсгэж болох файл. Дээр жишээ дэх object file бол үнэндээ relocatable object file ёом.
Executable object file: Санах ойруу шууд хуулаад ажилуулж болох файл. Програм гэдэг ч одоо яг энэ шүү дээ
Shared object file: За энэ бол ер нь relocatable object file, гэхдээ, санах ойруу шууд хуулаад ажиллаж байх үед нь link хийгээд ашиглаж болох файл. Ажиллаж байх үед болон яг ажилуулахын өмнө буюу load хийх үед link хийж болох файл. Dynamic Link гэж хэлдэг.
Компайлер болон ассемблерууд relocatable object file үүсгэдэг. За бас Shared Object File үүсгэдэг. Linker л executable object file үүсгэдэг.
Object Module гэж хэдэн byte--уудын дараалал, Object File гэж диск дээр байгаа, hard disk ч юмуу, object module юм. Гэхдээ мөн л энэ хоёрыг хольж сольж нэрийг нь ярьдаг.
Object File--ууд нь дотроо бас хэдэн форматын дагуу үүсгэгддэг. Хамгийн анхны хувилбар буюу Bell Lab--д хийгдсэн Unix системд a.out формат, Windows дээр Portable Executable буюу PE, Mac дээр Mach-O форматаар, linux дээр ELF буюу executable and linkable гэсэн форматаар хадгалагддаг. Энэ удаа бол ELF ашиглана, гэхдээ бусад системүүдэд зарчим нь бол яг адилхан шүү.
ELF header |
.text |
.rodata |
.data |
.bss |
.symtab |
.rel.text |
.rel.data |
.debug |
.line |
.strtab |
Section header table |
Хамгийн доор байгаа section header table--ээс дээд мөрүүдийг бүгдийг нь тус бүр нэг нэг section гээд ойлгочих. ELF header section нь 16 byte ба энэ нь 1 word size--н хэмжээ, byte ordering буюу нөгөө big ednian, little endian аль н болох, за тэгээд линкерт хэрэгтэй мэдээллүүдийг агуулна. Word size--н хэмжээг яагаад заадаг вэ гэхээр, уг нь бол компьютерт санах ойн 1 хаягт байрлах хэсэг битүүдийн уртыг илэрхийлдэг. Тэгтэл анх интел хэд cpu нь ч билээ 8 бит--г word гэж явсанаас болоод энэ анх тохирч ярьсан зүйл форматын дагуу яваагүй. Тэгээд жишээ нь CPU Register--үүд double word зэрэг үг хэллэг ашиглахаас өөр аргагүй болсон. Уг нь бол 1 word нь тухайн компьютер, машины 1 дор дамжуулж чадах битын урт буюу энэ нь санах ойн 1 хаягт агуулагдах битүүдийн урт байдаг.
За, дээрээс нь эхлээд тайлбарлаад явья
.text | Энэ нөгөө source code--ны чинь машинкод нь |
.rodata | Read-only датанууд. Жишээ нь printf--н форматууд |
.data | С--гийн global and static хувьсагчууд. Гэхдээ анхны утга оноосон шүү |
.bss | Яг .data шиг гэхдээ анхны утга оноогоогүй нь бюу initialize хийгдээгүй нь |
.symtab | Symbol table. Энэ бол тухайн энэ модульд амжилттай reference resolve хийгдсэн section. Энэ гэхдээ, функц доторх локал хувьсагчууд биш шүү. Локал хувьсагчууд stack--д хадгалагдаад, устгагдаад явдаг. -g флаг заавал шаардлагагүй, энэ table--г гаргахад |
.rel.text | .text дээр манай програмын код байгаа гэж хэлсэн. Linker ажиллаад өөр object file--ууд орж ирэхэд хаяг нь шинэчлэгдэх ёстой symbol--уудын жагсаалт. Энэ ер нь бол функц дуудалт, бас global хувьсагчууд бүгд хаяг нь шинэчлэгдэх ёстой, бодоод үзээрэй. Дахиад нэг юмыг бодоод үзээрэй, энэ секшн executable object file--д хамаагүй, яагаад гэвэл, угаса update хийцэн байхгүй юу кк |
.rel.data | Энэ бол тухайн энэ модулд хэрэглэж байгаа хувьсагч, функцын жагсаалт, гэхдээ энэ модульд тодорхойлогдоогүй. Модул гэхээр ер нь яг ажиллаж байгаа source file, object file--г хэлнэ. |
.debug | Дэбаг, тест хийхэд зориулсан секшн. Локал хувьсагч, typedef энэ тэр энд байна. Гэхдээ компайлдахдаа -g флагтай байх ёстой |
.line | Ориг анхны source file дах мөрийн дугаар болон .text секшнд байгаа коммандын тус бүрийн дугаар, харгалзуулсан. Мөн л -g флагтай байх ёстой |
.strtab |
|
Линкерийн үндсэн хийх ёстой үйлдэл нь:
Symbol Resolution: Програм бичихэд хувьсагч, функцууд маш их ашиглагддаг. Хувьсагчийн нэр болон функцын нэрийг object file дотор symbol гээд байгаа юм. Тэгэхээр линк хийхэд эдгээр symbol бүрийг юу юу гэдэгийг нь учрыг олж холбох ёстой.
Reolcation: Програм ажиллахад санах ой дотор байрласан байж байж л ажиллана. Програм гэдэг бол доод түвшиндээ CPU ний хийдэг хэдэн instruction буюу заавруудын дараалал л байдаг. Эдгээр заавар болгон нь санах ойн тэр хаяганд байна гэдэгийг нь линкер тодорхойлж зааж өгдөг. Компайлер болон ассемблер нь програмын эхнээс 0 гэсэн хаягнаас л хамааралтайгаар relocatable object file үүгэдэг.
Symbol, Symbol Table:
Тухайн нэг Relocated Object File--г модуль гээд байгаа. M модул гэе. Тэгэхээр М модуль бүр тухайн модульд тодорхойлогсон symbol эсвэл гаднаа өөр модульд тодорхойлогдсон гэхдээ энэ М модульд зөвхөн ашиглагдаж байгаа symbol--ууд байгаа. Эдгээр symbol--н тайлбар мэдээллүүдийг агуулсан жагсаалтыг symbol table гэнэ.
Symbol--н хувьд 3 төрлийн симбол байж болно:
Global Symbol: Энэ нь тухайн М модульд тодорхойлогдсон, өөр модулиуд ашиглах боломжтой symbol. С дээр static биш функцууд, мөн global хувьсагчууд юм
Global Symbol: Энэ нь тухайн М модульд тодорхойлогдоогүй, зүгээр л уг модульд ашиглаж байгаа symbol юм. С дээр бол external гэж нэрлэдэг.
Local Symbol: Тухайн М модульд тодорхойлогдсон мөн зөвхөн тухайн модуль М--дээ л ашиглагддаг symbol--г хэлнэ. С дээр бол static функц, хувьсагчууд.
Дахин анхааруулахад, локал хувьсагчууд бол програм ажиллах үед стак санах ойн зохицуулалтаар зохицуулагддаг, линкерт хамаагүй шүү.
Бас нэг сонирхолтой зүйл байгаа, локал static хувьсагчууд програмын стакаар биш энэ линкерээр зохицуулагддаг. Доорх жишээ кодыг хараарай:
m.c |
Int f() { static int x = 0; return x; } Int g() { static int x = 1; return x; } |
Ассемблэр болон линкер дээр 2ууланд нь хоёр өөр нэртэй symbol ирнэ. x.1 x.2 ч гэх юмуу.
.symtab секшин--д симболууд дараах бүтэцтэй жагсаалт байдлаар байдаг.

name нь string table доторх байршил. Энэ нь null--аас төгссөн тэмдэгт мөрлүү заана.
value нь хаяг. Аль секшинд байгаагаас хамаараад тухайн секшиндэх байршил relocatble object file--н хувьд, харин executable object file байвал санах ойн хаяг.
Size бол тухайн symbol--н хэмжээ. Мэдээж byte--аар хэмжигдэнэ. Type нь бол data, func, section гэх утга авах, төрөл гэсэн үг.
Binding нь локал эсвэл глобал гэдэгийг илтгэнэ.
Section нь аль секшинд байгааг илтгэх тоо. Секшинийг дугаарладаг, .text --г 1 гээд л эхлэнэ.
Бүх symbol тухайн модулийхаа аль нэг секшинд харгалзах ёстой. Зохиомол гурван секшин байгаа, тэр нь:
ABS: Энэ нь байршилыг нь сольж болохгүй симбол
UNDEF: Энэ нь уг модульд ашиглагдаж байгаа, гэхдээ өөр модульд тодорхойлогдсон симбол
COMMON: Анхны утга оноогоогүй хувьсагч
GNU readelf: Энэ програмаар object file--уудыг уншихад зүгээр. main.o файлыг уншиж үзье.

9 дээр жишээ нь манай main функц, 1 гэсэн секшин буюу .text дотор хамгийн эхэнд буюу 0 хаяг дээр 26 бит хэмжээтэй функц симбол байх нь. Дараа нь манай массив 8 байт хэмжээтэй, int=4byte, .дата секшинд байгаа. Хамгийн доор sum. Sum нь манайд тодорхойлогдоогүй зөвхөн ашиглагдаж байгаа симбол байх нь.
Symbol Resolution:
Дээр хэлсэнчлэн линкерийн эхний хамгийн чухал үйлдэл нь энэ симболуудын учрыг олох. Хамгийн чухал дүрэм бол 1 симбол яг 1 тодорхойлогч байх ёстой. Линкерийг дуудаж ажилуулахдаа оролтын файлууд нь дан object file байх ёстой. Тэр хэдэн файлуудаасаа л тодорхойлогч болон симболуудыг хооронд нь холбоно. Локал симболын хувьд бол амархан, компайлер учрыг нь олцон аваад ирнэ.
Харин глобал симбол л хамгийн төвөгтэй нь.
Хэрэвээ линкер тухайн модульд тодорхойлогдоогүй симбол олвол өөр модулиудад байгаа гэж үзээд линкертээ энэнийхээ учрыг ол гээд symbol table--д оруулаад орхидог. Хэрвээ линкер бусад модуль дотроос альнаас нь ч тохирох тодорхойлогч олохгүй бол учир битүүлэг алдааны мэссэж өгөөд зогсдог. Жишээ нь:


Symbol resolution--г төвөгтэй болгодог бас нэг тохиолдол нь тухайн симболд тохирсон тодорхойлогч хэд хэд олдох. Жишээ нь 2 өөр модульд 1 ижил нэртэй глобал функцууд байж болно.
foo.c | faa.c |
int main() { return 0; } | int main() { return 0; } |

Яг дээрх асуудлыг linux linker яаж шийддэг вэ гэхээр, бусад нь ч гэсэн адилхан шүү. Линкер програм ажилуулахад хэд хэдэн object file--аас бүрдэнэ. Object file болгон өөрсдийн symbol table байгаа. Ассемблер нь анхны утга оноосон хувьсагч бас функцуудын симболд ‘strong’ гэсэн төрөлд оруулдаг. Харин анхны утга оноогоогүй глобал симболуудыг ‘weak’ гэсэн төрөлд оруулдаг.
Дээрх 2 төрөлийг ашиглаад linux linker дараах алгоритмаар давхар тодорхойлогч олдсон симболуудыг зохицуулдаг:
Хэрэв ижил нэртэй мөн хоёулаа strong төрөлийх байвал шууд алдаа өгөө зогсооно
Хэрэв ижил нэртэй мөн нэг нь strong нөгөөх нь weak байвал шууд strong симболыг сонгоно
Хэрэв ижил нэртэй мөн хоёулаа weak төрөлийх бол дурын аль симболыг нь сонгоно
Жишээ:
foo.c | faa.c |
int x = 1111; int main() { return 0; } | int x = 99999; int main() { return 0; } |
Дээрх жишээ нь дээр х нь strong төрөлийг 2 object file--н symbol table--д хоюуланд нь агуулж байгаа учир multiple definition гэсэн алдааг заана. Харин аль нэг файлд х--н анхны утгыг өгөөгүй байсан бол энэ алдаа гарахгүй, програм шууд асуудалгүй ажиллана. Жишээ нь:
foo.c | faa.c |
include <stdio.h> void f(void); int x = 123; int main() { f(); printf(“x = %d\n”, x); return 0; } | int x; void f() { x = 9999; } |
Дээрх жишээ юуг хэвлэх вэ? x = 9999 гэж хэвлэнэ.
shell>> gcc -o pp foo.c faa.c
shell>> ./pp
shell>> 9999
WHAAT? Уг нь нээх гайхаад байх юм биш, гэхдээ main функцын зохиогч талаас бол маш аюултай. Яагаад гэвэл линкер энэ давхар тодорхойлогчийн талаар анхааруулга ч өгөлгүй ажилуулчиж байгаа биз? Дээрх дүрэм дээр бичигдсэн 2 болон 3 бол маш том асуудлын голомт байгаа юм. Бүр том шүү.
Одоо ижил симбол хоюулаа weak төрөл үед байхыг үзье.
foo.c | faa.c |
#include <stdio.h> void f(void); int x; int main() { x = 15213; f(); printf(“x = %d\n”, x); return 0; } | int x; void f() { x = 15212; } |
Ажилуулаад үзээрэй, бүр аюултай дараах жишээ байна:
foo.c | faa.c |
#include <stdio.h> void f(void); int y = 15212; int x = 15213; int main() { f(); printf(“x = %d, y = %y\n”, x, y); } | double x; void f() { x = -0.0; } |
shell>> gcc -o ff foo.c faa.c

shell>>./ff
shell>> x = 15213, y = -21…….
За одоо асар олон тооны object file--г линкерт оруулаад ажилуулж байна гээд төсөөлөөд үз. Тэгтэл линкер нь зүгээр л анхааруулаад эсвэл зарим тохиолдолд анхааруулга өгөхгүй шууд ажилуулна. Програм чинь шал өөрөө ажиллаад унах нь. Одоо яаж алдаагаа олох вэ? Ийм л аюултай зүйл байгаан. Linker ажилуулахдаа -fno-common гэх флаг ашиглавал олон тодорхойлогч олдвол зогсоо гэсэн флаг бас байгаа. -Werror флаг нь болохоор бүх warning--уудыг алдаа болгоод зогсоо гэсэн флаг.
За энэ удаагийн постоо энэ хүрээд өндөрлөе. Дараагийн постоороо library хэрхэн холбох вэ, ажиллаж байх үед хэрхэн library холбох гээд тэрний дараагийн постоор ebarimt--н сангуудтэй хэрхэн ажиллах талаар оруулаад энэ бүлэг пост дуусах юм.