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.

results matching ""

    No results matching ""