Kod Referanslarının Ele Alınması
Bu bölüm daha sonraki incelemelerimize bir alt yapı niteliği taşımaktadır. İlk olarak derleme zamanında tanımı bulunan içsel (internal) referansların nasıl ele alındığına bakalım.
Kaynak kod içindeki referanslar kod ve data belleğindeki alanlara karşılık gelmektedir. Fonksiyon ve değişken isimleri, en nihayetinde birer adrese dönüşmesi beklenen, kaynak kod düzeyinde referanslardır. Derleme aşamasında, çoğu durumda, kaynak kod içerisindeki referanslar nihai adreslere dönüştürülemezler.
Derleyici oluşturduğu sembolik makina kodlarına, herhangi bir adres işlemiyle ilgilenmeksizin, yalnız referans isimlerini, tiplerini ve bilinirlik düzeylerini not etmektedir. Sonrasında assembler, .o uzantılı, ELF formatındaki, amaç dosyayı (relocatable object file) oluşturmaktadır. assembler, makina kodlarına ilave olarak, ELF dosyasında, ilgilendiği referanslara ilişkin kayıtların tutulduğu bir sembol tablosu da oluşturur. assembler çoğu durumda sembollere, bağlayıcının nihai adreslerle değiştirilmesi beklenen, geçici özel adresler atar. Dışsal bağlanıma kapalı statik fonksiyonlara ise nihai adresleri assembler tarafından atanabilmektedir. Aslında atanan bu adres fonksiyonun gerçek adresi değil, yer değişimini gösteren görece bir adrestir. Bu konuya daha sonra değineceğiz. Basit bir örnek üzerinden incelememize devam edelim.
Örnek kodları sırasıyla test.c, tar.c olarak isimlendirip 32 bit hedefli olarak derleyelim.
#include <stdio.h>
void tar();
void foo() {
}
static bar() {
}
int main() {
foo();
bar();
tar();
return 0;
}
#include <stdio.h>
void tar() {
}
$ gcc -otest test.c tar.c -m32 --save-temps
test.c uygulamasının sırasıyla foo, bar ve tar fonksiyonlarını çağırdığını görmekteyiz. tar fonksiyonunun tanımının başka modülde (tar.c) olduğuna ve bar fonksiyonunun static olarak tanımlandığına dikkat ediniz. test.c için üretilen sembolik makina kodlarında bu fonksiyonların nasıl geçtiğine bakalım.
$ cat test.s | grep foo
.globl foo
.type foo, @function
foo:
.size foo, .-foo
call foo
$ cat test.s | grep bar
.type bar, @function
bar:
.size bar, .-bar
call bar
$ cat test.s | grep tar
call tar
Nokta ile başlayan komutlar, gerçek makina komutlarına karşılık gelmeyen, assembler'ı bilgilendirme amaçlı kullanılan direktiflerdir. Sembolik makina çıktısında .type ve .globl direktiflerini görmekteyiz. type ile referansın tipi, globl ile referansın bilinirlik alanı gösterilmektedir. Derleyici, test.c dosyasında foo ve bar tanımlarını gördüğü için ilgili referansların fonksiyon olduğunu not etmiş. Ayrıca static olarak tanımlanan bar fonksiyonunun dışsal bağlanıma kapalı olduğunu, foo fonksiyonunun ise .globl direktifi sayesinde global bilinirlik alanına sahip olduğunu görmekteyiz. Başka modüldeki tar fonksiyonuna ilişkin ise yalnız fonksiyon çağrısını görmekteyiz. Şimdi assembler'ın sembolik makina kodlarına ilişkin oluşturduğu gerçek makina kodlarına bakalım. Bu amaçla objdump aracını kullanacağız. test.o için makina kodları aşağıdaki gibidir.
Not: Sadeleştirme amacıyla, sembolik makina kodlarını incelerken .cfi ile başlayan assembler direktiflerini göz ardı edeceğiz.
$ objdump -d test.o test.o: file format elf32-i386 Disassembly of section .text: 00000000 < foo >: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 5d pop %ebp 4: c3 ret 00000005 < bar >: 5: 55 push %ebp 6: 89 e5 mov %esp,%ebp 8: 5d pop %ebp 9: c3 ret 0000000a < main >: a: 8d 4c 24 04 lea 0x4(%esp),%ecx e: 83 e4 f0 and $0xfffffff0,%esp 11: ff 71 fc pushl -0x4(%ecx) 14: 55 push %ebp 15: 89 e5 mov %esp,%ebp 17: 51 push %ecx 18: 83 ec 04 sub $0x4,%esp 1b: e8 fc ff ff ff call 1c < main+0x12 > 20: e8 e0 ff ff ff call 5 < bar > 25: e8 fc ff ff ff call 26 < main+0x1c > 2a: b8 00 00 00 00 mov $0x0,%eax 2f: 83 c4 04 add $0x4,%esp 32: 59 pop %ecx 33: 5d pop %ebp 34: 8d 61 fc lea -0x4(%ecx),%esp 37: c3 ret
Sol tarafta gerçek sağ tarafta ise sembolik makina kodları listelenmektedir. Sırasıyla foo, bar ve tar fonksiyon çağrılarına ilişkin komutlar aşağıdaki gibidir.
1b: e8 fc ff ff ff call 1c <main+0x12>
20: e8 e0 ff ff ff call 5 <bar>
25: e8 fc ff ff ff call 26 <main+0x1c>
2a:
e8, sembolik call komutuna karşılık gelen gerçek makina kodu, sonraki 4 byte ise adres bilgisidir. e8 makina komutu, fonksiyonun mutlak adresini almak yerine, komut göstericisinin (Instruction Pointer, Program Counter) gösterdiği değere görece bir adres almaktadır (IP Relative Addressing, PC Relative Addressing). Komut göstericisi, bir komut işletilirken, bir sonraki komutun başlangıç adresini tutmaktadır.
Statik bar fonksiyonu için e0, diğer fonksiyonlar için ise fc ile başlayan ve ff ile devam sayılar görüyoruz. Bu gösterimde e8 komutunun sağındaki ilk byte en düşük anlamlı byte'ı göstermektedir (Little Endian). Bu sayılar işaretli olarak ele alınmaktadır, en yüksek anlamlı bit değerinin 1 olması sayının negatif olduğunu göstermektedir. Negatif sayılar bellekte ikiye tümleyen (Two's Complement) şeklinde tutulmaktadır. Bu durumda fc ff ff ff ve e0 ff ff ff sayıları sırasıyla -0x4 ve -0x20 sayılarına karşılık gelmektedir.
Not: Bir sayının ikiye tümleyenini bulmak için, ikili sayı sisteminde temsil edilen sayının, 1 olan bitleri 0 ve 0 olan bitleri 1 yapılarak önce bire tümleyeni alınır. Sonrasında elde edilen sonuç 1 ile toplanarak ikiye tümleyenine ulaşılır.
Amaç kodlar içinde oldukça sık rastlanan fc sayısının rastgele bir sayı olmadığına dikkat ediniz. Bir sonraki makina komutunun adresinden 4 byte geri geldiğimizde, ilgili makina komutunun başlangıç adresinden bir sonraki adrese ulaşmaktayız. Böyle bir çağrı yapılması durumunda işlemci üzerinde illegal instruction hatası oluşacağı açıktır. fc ile başlayan bu değerler, daha sonra bağlayıcı tarafından değiştirilmesi beklenen geçici değerlerdir.
foo ve tar fonksiyonları için bu değerler objdump tarafından hesaplanarak, call komutunun operandı olarak, 1c ve 26 şeklinde gösterilmektedir. Statik bar fonksiyonu için 5 değerine ulaşıldığına ve bar fonksiyonunun 5 numaralı adresten başladığına dikkat ediniz.
nm ve readelf araçları ile amaç dosya içindeki sembolleri aşağıdaki gibi listeleyebiliriz.
$ nm test.o
00000005 t bar
00000000 T foo
0000000a T main
U tar
Tarif | Anlamı |
---|---|
t | Fonksiyon tanımının ilgili modül içinde olduğunu fakat dışsal bağlanıma kapalı olduğunu gösterir |
T | Fonksiyon tanımının ilgili modül içinde olduğunu ve dışsal bağlanıma açık olduğunu gösterir |
U | Fonksiyon tanımının ilgili modül içinde olmadığını gösterir |
Benzer sonuçlara readelf ile aşağıdaki gibi ulaşabiliriz. Dışsal bağlanıma kapalı bar LOCAL olarak gösterilmektedir.
$ readelf -s test.o
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
...
5: 00000005 5 FUNC LOCAL DEFAULT 1 bar
...
9: 00000000 5 FUNC GLOBAL DEFAULT 1 foo
10: 0000000a 46 FUNC GLOBAL DEFAULT 1 main
11: 00000000 0 NOTYPE GLOBAL DEFAULT UND tar
Dana önce, ELF formatındaki amaç dosya içinde assembler tarafından oluşturulan, bir sembol tablosu tutulduğunu söylemiştik. ELF içinde ayrıca, assembler tarafından geçici adres verilen referanslara ilişkin bilgilerin tutulduğu yeniden konumlandırma (relocation) bölümü de bulunmaktadır.
Bağlayıcı nihai adreslerini atayacağı sembolleri relocation bölümüne bakarak bulmaktadır. relocation bölümünü aşağıdaki gibi listeleyebiliriz. relocation bölümünde, statik bar fonksiyonunun bulunmadığına dikkat ediniz.
$ readelf -r test.o
Relocation section '.rel.text' at offset 0x24c contains 2 entries:
Offset Info Type Sym.Value Sym. Name
0000001c 00000902 R_386_PC32 00000000 foo
00000026 00000b02 R_386_PC32 00000000 tar
Şimdi bağlanma aşamasından sonra uygulamanın makina kodlarına bakalım.
080483eb <foo>:
80483eb: 55 push %ebp
80483ec: 89 e5 mov %esp,%ebp
80483ee: 5d pop %ebp
80483ef: c3 ret
080483f0 <bar>:
80483f0: 55 push %ebp
80483f1: 89 e5 mov %esp,%ebp
80483f3: 5d pop %ebp
80483f4: c3 ret
080483f5 <main>:
...
8048406: e8 e0 ff ff ff call 80483eb <foo>
804840b: e8 e0 ff ff ff call 80483f0 <bar>
8048410: e8 0e 00 00 00 call 8048423 <tar>
...
08048423 <tar>:
8048423: 55 push %ebp
8048424: 89 e5 mov %esp,%ebp
8048426: 5d pop %ebp
8048427: c3 ret
Fonksiyonlara gerçek adresleri atanmasına karşın, bar fonksiyonu çağrısına ilişkin makina komutunun değişmediğini görüyoruz. İlgilendiğimiz fonksiyonların adresleri belirlendiğinden (symbol resolution) artık relocation bölümünde kayıtları bulunmamaktadır.
$ readelf -r test | grep foo
Bir uygulamanın paylaşımlı bir kütüphaneye bağımlı olması durumunda ise, uygulama içerisinde, tanımı kütüphanede olan, dışsal (external) referanslar olacaktır. Derleme zamanında bağlayıcı tarafıdan çözümlenemeyen bu referansların, yükleme zamanında dinamik bağlayıcı tarafından çözümlenmesi beklenmektedir.