> Dosyalarda "Close-on-exec" bayrakları: Öyle bir bayraktır ki "exec" yapıldığında ilgili açık olan dosyanın "close" edileceğini belirtir ve her bir "fd" için bir bayrak vardır. Bu bayraklar prosesin kontrol bloğu içerisinde yer alır. Bu bayrağın "set" edilmesi demek, "exec" işlemi sırasında ilgili dosyaların da otomatik olarak kapatılacağı demektir. "file" nesneleri yok edilmiyor, sadece dosyalar kapatılıyor. Yani "file" içerisindeki sayaçlar bir azaltılıyor. Bu sayaçlar sıfıra düştüğünde yok ediliyor. Varsayılan durumda bu bayrak "set" EDİLMEMİŞTİR. Pekiyi bizler bu bayrağı nasıl "set" ederiz? Birinci yöntem dosyayı ilk açışımız sırasında özel bir bayrak kullanmamız ki bunun adı "FD_CLOEXEC" biçimindedir. İkinci yöntemde ise "fcntl" isimli fonksiyonu çağırmamız. -> Dosyayı ilk açış sırasında ilgili bayrağı "set" etmek: int fd; fd = open("test.txt", O_RDONLY | FD_CLOEXEC); -> "fcntl" isimli fonksiyonu ile "set" etmek: int fd; fd = open("test.txt", O_RDONLY); if(fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC) == -1) exit_sys("fcntl"); -> "fcntl" isimli fonksiyonu ile "reset" etmek: int fd; fd = open("test.txt", O_RDONLY); if(fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) & ~FD_CLOEXEC) == -1) exit_sys("fcntl"); Pekiyi bizler bu bayrağı "set" etmesek, "exec" ile çalıştırılan proses evvelki proses dair "fd" niteleyicilerini nasıl bilebilir? Yöntemlerden biri komut satırı argümanlarını kullanmak, diğeriyse prosesler arası haberleşme tekniklerini kullanmak. * Örnek 1, "main" programında açılan dosyaya ait "fd" betimleyicisi, "mample" programına komut satırı olarak gönderilmiştir. /* main.c */ #include #include #include #include #include void exit_sys(const char* msg); int main(void) { printf("Main program started running!...\n"); int fd; if((fd = open("main.c", O_RDONLY)) == -1) exit_sys("open"); char buffer[10 + 1]; sprintf(buffer, "%d", fd); if(execl("mample", "mample", buffer, (char*)0) == -1) exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include int main(int argc, char** argv) { int fd; fd = strtol(argv[1], NULL, 10); char buffer[100 + 1]; ssize_t result; if((result = read(fd, buffer, 100)) == -1) { perror("read"); exit(EXIT_FAILURE); } buffer[result] = '\0'; close(fd); } * Örnek 2, Aşağıdaki örnekte ilgili bayrak "set" edildiği için artık yeni proses okuma yapamayacaktır. /* main.c */ #include #include #include #include #include void exit_sys(const char* msg); int main(void) { printf("Main program started running!...\n"); int fd; if((fd = open("main.c", O_RDONLY | FD_CLOEXEC)) == -1) exit_sys("open"); char buffer[10 + 1]; sprintf(buffer, "%d", fd); if(execl("mample", "mample", buffer, (char*)0) == -1) exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include int main(int argc, char** argv) { int fd; fd = strtol(argv[1], NULL, 10); char buffer[100 + 1]; ssize_t result; if((result = read(fd, buffer, 100)) == -1) { perror("read"); exit(EXIT_FAILURE); } buffer[result] = '\0'; close(fd); } * Örnek 3, Aşağıdaki örnekte "FD_CLOEXEC" bayrağı "set" edilmiştir: * main.c */ #include #include #include #include #include void exit_sys(const char* msg); int main(void) { printf("Main program started running!...\n"); int fd; if((fd = open("main.c", O_RDONLY)) == -1) exit_sys("open"); if(fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC) == -1) exit_sys("fcntl); char buffer[10 + 1]; sprintf(buffer, "%d", fd); if(execl("mample", "mample", buffer, (char*)0) == -1) exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include int main(int argc, char** argv) { int fd; fd = strtol(argv[1], NULL, 10); char buffer[100 + 1]; ssize_t result; if((result = read(fd, buffer, 100)) == -1) { perror("read"); exit(EXIT_FAILURE); } buffer[result] = '\0'; close(fd); } > Yorumlayıcı Dosyalarının Çalıştırılması: "exec" fonksiyonları bir dosyayı çalıştırmadan evvel ilk olarak bu dosyanın çalıştırılabilir bir dosya olup olmadığını sorguluyor. Eğer çalıştırılabilir bir dosya değilse, örneğin ilgili dosyanın bir "text" dosyası olduğunu varsayalım (Linux dünyasında çalıştırılabilir dosyalar "ELF" ya da "a.out" formatındadır), bu durumda bu dosyayı açıp ilk satırı, "\n" karakteri görene kadar okuyor. Okumuş olduğu karakterler ise aşağıdaki kalıba uygun olmalıdır: #! [args] Buradaki "#!" karakterleri birleşik olmalıdır. karakterinden önceki boşluk karakteri opsiyonel bir karakterdir. Yani "#!" ile arasında bir boşluk oladabilir, olmayadabilir. Fakat ile [args] arasında en az bir boşluk olmalıdır. [args] ise birden fazla argümanı temsilen sadece bir defa yazılmıştır, yani [args] yazan yere boşluklarla ayrılmış argümanlar gelebilir. Son olarak "#!" karakterinden evvel boşluk olmamalı ve bu iki karakter bitişik yazılmalıdır. Buradaki "#!" karakterlerine ise literatürde "shebang", "hashbang", "sharp-exclamation" gibi isimler verilmiştir. Eğer birinci satırda yazılanlar bu kalıba uyuyorsa, "exec" fonksiyonları yol ifadesi ile belirtilen dosyayı çalıştırmaya çalışacaklar ve [args] bölümünde belirtilen argüman(lar)ı bu fonksiyona argüman olarak geçecektir. Bütün bu işlemler "kernel" içerisinde gerçekleşmektedir. Tabii buradaki ile belirtilen dosyanın, bu dosyayı çalıştırmak isteyen prosese "x" hakkını da vermiş olması gerekmektedir. * Örnek 1, Aşağıdakiler "shebang" kalıbına uygundur: #! /bin/bash #!/bin/bash #! /usr/bin/python #! make -f * Örnek 2, Aşağıdaki örnekte "shebang" içermeyen bir metin dosyası çalıştırılmak istenmiştir: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # execl: Exec format error */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Prosesimizin "test.txt" dosyası üzerinde "x" hakkına * sahip olması gerekmektedir. */ if(pid == 0 && execl("test.txt", "test.txt", "ali", "veli", "selami", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* test.txt */ Merhaba Dunya * Örnek 3, Aşağıdaki program ile "test.txt" dosyası çalıştırılmak istenmiş fakat "shebang" karakterlerinden dolayı "sample" programı çalıştırılmıştır. /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Sample is running... Optional_Argument /home/sample test.txt ali veli selami */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Görüldüğü üzere "execl" fonksiyonuna geçilen ikinci argüman, "sample" programına * geçilmemiştir. Bunun sebebi, "shebang" sırasında "execl" fonksiyonuna geçilen birinci * argümandan sonraki argümanın es geçilmesidir. */ if(pid == 0 && execl("test.txt", "test.txt", "ali", "veli", "selami", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* test.txt */ #! /home/kaan/sample Optional_Argument /* sample.c */ #include int main(int argc, char** argv) { puts("Sample is running..."); for(int i = 0; i < argc; ++i) { puts(argv[i]); } } Bu örneğe bakarak diyebiliriz ki esas çalıştırılmak istenen "test.txt" dosyasıdır. Bu dosyanın ilk satırında "shebang" karakterleri olduğu için, "sample" programı çalıştırılacaktır. "sample" programına geçilen argümanların sırası ise şu şekildedir; "sample" programındaki <----> İfade ettiği şey argv[0] <----> "shebang" karakterleriyle birlikte yazılan ifadesinin kendisi. argv[1] <----> "shebang" karakterleriyle birlikte yazılan isteğe bağlı [argv] argümanları (eğer varsa). argv[2] <----> "main" programı içerisinde çağrılan "exec" fonksiyonlarına geçilen birinci argüman. argv[3] ... argv[n] <----> "main" programı içerisinde çağrılan "exec" fonksiyonlarına geçilen üçüncü, dördüncü vb. diğer komut satırı argümanları. Eğer böyle bir argüman geçmemişsek, bu indisten sonraki yazılar da boş kalacaktır. Dikkat edilmesi gereken nokta, bu fonksiyona geçilen ikinci argüman es geçilecektir. * Örnek 4, /* test.txt */ #! /home/kaan/sample ankara /* main.c */ //... int main(void) { /* # OUTPUT # Sample is running... /home/kaan/sample ankara test.txt ali veli selami */ //... if(pid == 0 && execl("test.txt", "test.txt", "ali", "veli", "selami", (char*)0) == -1) exit_sys("execl"); //... return 0; } //... /* sample.c */ //... int main(int argc, char** argv) { puts("Sample is running..."); for(int i = 0; i < argc; ++i) { puts(argv[i]); } } * Örnek 5, /* test.txt */ #! /home/kaan/sample ankara /* main.c */ //... int main(void) { /* # OUTPUT # Sample is running... /home/kaan/sample ankara test.txt veli selami */ //... if(pid == 0 && execl("test.txt", "ali", "veli", "selami", (char*)0) == -1) exit_sys("execl"); //... return 0; } //... /* sample.c */ //... int main(int argc, char** argv) { puts("Sample is running..."); for(int i = 0; i < argc; ++i) { puts(argv[i]); } } Burada dikkat etmemiz gereken bir diğer nokta da "shebang" satırının toplam uzunluğudur. Bu uzunluk 255 karakter ile sınırlanmış olup, 256. karakter olarak "\n" karakteri gelmektedir. Diğer yandan "shebang" karakterleriyle belirtilen dosyanın yol ifadesi mutlak ya da göreli yol ifadesi olabilir. Göreli bir yol ifadesi kullanılması durumunda, "exec" fonksiyonlarını çağıran prosesin "Current Working Directory" konumu baz alınacaktır. Örneğin, #! sample biçiminde yazılan "shebang" satırına göre, "sample" ismi o prosesin "Current Working Directory" konumunda aranacaktır. Fakat tavsiye edilen yöntem ise mutlak yol ifadesi kullanılmasıdır. Öte yandan "shebang" satırında belirtilen [args] kısmına yazacağımız argümanların sayısı arasında standart bir yöntem yoktur. UNIX türevi işletim sistemlerinde bu biçim, işletim sistemini yazan kimselere bırakılmıştır. Bazı sistemler boşluklarla ayrılmış birden fazla argümanı kabul ederken, bazıları sadece bir adet argüman kabul etmektedir. Örneğin, aşağıdaki "shebang" satırındaki [args] kısmında bulunanlar Linux sistemlerinde tek bir argüman olarak ele alınmaktadır; #! /home/kaan/sample ankara istanbul izmir Yani "sample" programını çalıştırdığımız vakit "ankara istanbul izmir" argümanları, "argv[1]" olarak aktarılacaktır. * Örnek 1, /* test.txt */ #! /home/kaan/sample ankara istanbul izmir /* main.c */ //... int main(void) { /* # OUTPUT # Sample is running... /home/kaan/sample ankara istanbul izmir test.txt ali veli selami */ //... if(pid == 0 && execl("test.txt", "test.txt, "ali", "veli", "selami", (char*)0) == -1) exit_sys("execl"); //... return 0; } //... /* sample.c */ //... int main(int argc, char** argv) { puts("Sample is running..."); for(int i = 0; i < argc; ++i) { puts(argv[i]); } } Fakat aynı "shebang" satırını başka UNIX türevi işletim sistemlerinde kullandığımız vakit sadece "ankara" argümanı ilgili prosese aktarmakta, diğerlerini elimine etmektedir. Bazı başka işletim sistemleri ise bu argümanları ayrı ayrı ilgili prosese aktarılmaktadır. İş bu farklılıklardan ötürü bizlerin sadece bir adet argümanı "shebang" satırında belirtmemiz uygun olacaktır. "shebang" satırının bir diğer kullanım yeri de "shell" programı üzerinden "script" dosyalarını çalıştırmaktır. * Örnek 1, Bir "script" dosyasının direkt çalıştırılması: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # 1 2 3 4 5 6 7 8 9 10 */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * "sample.sh" dosyası çalıştırılmak istenmiştir. Fakat bu dosyadaki "shebang" * satırında herhangi bir argüman kullanılmamıştır. "execl" fonksiyonuna da * herhangi bir argüman geçilmemiştir. Bu durumda "shell.sh" içerisindeki * "script" kodları çalıştırılmıştır. "shell" script dosyalarında "#" karakteri * yorum satırı olarak işlev gördüğünden, herhangi bir sonsuz döngü meydana * gelmemiştir. Prosesimizin, bu şekilde çalıştırdığımız "sample.sh" dosyası * üzerinde "x" hakkında sahip olmasına gerek yoktur. Fakat bizler "shell" * programının kendisi üzerinden, yani "./sample.sh" komutu ile, ilgili "script" * dosyasını çalıştırırsak artık "x" hakkında sahip olmamız gerekmektedir. */ if(pid == 0 && execl("sample.sh", "sample.sh", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* sample.sh */ #! /bin/bash for i in {1..10} do echo $i done * Örnek 2, Bir "script" dosyasının dolaylı yoldan çalıştırılması: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # 1 2 3 4 5 6 7 8 9 10 */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid == 0 && execl("test.txt", "test.txt", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* test.txt */ #! /bin/bash sample.sh /* sample.sh */ for i in {1..10} do echo $i done * Örnek 3, Bir "python" kodunun çalıştırılması: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # 0 1 2 3 4 */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Yine burada "test.txt" dosyasını kullanmadan, tıpkı birinci örnekte olduğu gibi, * direkt olarak "sample.py" dosyasını da çalıştırabiliriz. */ if(pid == 0 && execl("test.txt", "test.txt", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* test.txt */ #! /bin/python3 sample.py /* sample.py */ for i in range(5): print(i) Pekiyi çalıştırmak istediğimiz dosyanın ilk satırında "shebang" yoksa ya da hatalı bir şekilde yazılmışsa ne olacak? Bu durumda "exec" fonksiyonları "-1" ile geri dönüp, "errno" değişkenine uygun değer atayacaktır. Diğer yandan "exec" fonksiyonlarının "p" li versiyonları, örneğin "execlp" ve "execvp", çalıştırılmak istenen dosyanın başında "shebang" görmezlerse sanki dosyanın başında aşağıdaki gibi bir "shebang" varmış gibi davranırlar; #! /bin/sh Öte yandan çalıştırılmak istenen dosyanın da prosese "x" hakkını vermiş olması gerekmektedir. Buradan hareketle diyebiliriz ki "shell" programı üzerinden "script" dosyalarını çalıştırırken başında "shebang" olmasına gerek yoktur eğer "p" li "exec" fonksiyonları kullanırsak. Diğer versiyonlarda böyle bir davranış söz konusu değildir. "p" li versiyonların sahip olduğu bu davranış 2017 POSIX standartlarından itibaren zorunlu kılınmıştır. Son olarak buradaki "sh" programı standartlarca kesin olarak çalışacak program değildir. Başka sistemlerde başka yorumlayıcı programlar çalışabilir. * Örnek 1, /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # 1 2 3 4 5 */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * "p" li versiyonlar, dosya ismi içerisinde "/" karakteri olmasına * rağmen işlev görmektedirler. */ if(pid == 0 && execlp("./sample.sh", "./sample.sh", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* sample.sh */ for i in 1 2 3 4 5 do echo $i done POSIX standartlarınca "shebang" satırındaki dosyasının da bir yorumlayıcı dosyasının olması durumunda neler olacağı garanti altına alınmamıştır fakat Linux işletim sisteminde dört kademeye kadar gidilmesine izin verilmiştir. Yani "recursive" biçimde "shebang" uygulamak için sınırımız dört adettir. Fakat tavsiye edilen yöntem, bundan kaçınmamızdır. > "system" fonksiyonu, standart bir C fonksiyonudur. "non-interactive shell" programını çalıştırır. Yani normal "shell" programından bir komut çalıştırırken "-c" komutunu kullanma gibidir. Bir diğer değişle ilgili komutu bir defa çalıştırır ve kendisini sonlandırır. Örneğin, aşağıdaki komutu "shell" programı üzerinde çalıştıralım; bash -c "ls -l; wc sample.c" "bash" programı çalıştırılacak, prosesin "Current Working Directory" konumunda bulunan dosyalar detaylarıyla birlikte ekrana basılacak, devamında ise "sample.c" dosyasındaki karakterlerin adedi ekrana basılacaktır. Daha sonra da "bash" programı sonlanacaktır. İşte "system" fonksiyonu da argüman olarak aldıklarını "bash" programı ile bir defa çalıştırmaktadır. Yine bu fonksiyon da bünyesinde "fork" ve "exec" işlemlerini gerçekleştirmektedir. Fonksiyonun imzası ise aşağıdaki gibidir; #include int system(const char *command); Bu fonksiyon argüman olarak çalıştırılmak istenen "shell" komutlarını yazı biçiminde almakta ve uygun bir geri dönüş değeri ile geri dönmektedir. Geri dönüş değerinin detaylarına ilerleyen derslerde değineceğiz. * Örnek 1, Aşağıdaki program ile "shell" üzerinden "ls -l; wc main.c" komutunu çalıştırmamız aynı şeydir: #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14100 14100 16080 Feb 24 23:18 a.out -rwxrwxrwx 1 root root 274 Feb 24 23:18 main.c 21 33 274 main.c */ system("ls -l; wc main.c"); // system("gcc main.c -o my_main"); // system("gcc main.c -o mymain2"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Temsili bir "system" implementasyonu: #include #include #include #include void my_system(const char* cmd); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14034 14034 16192 Feb 24 23:21 a.out -rwxrwxrwx 1 root root 630 Feb 24 23:21 main.c */ my_system("ls -l"); return 0; } void my_system(const char* cmd) { pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid == 0 && execlp("bash", "bash", "-c", cmd,(char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bu fonksiyon ile "shell" programında çalıştırabileceğimiz bütün komutları bir C programı ile de gerçekleştirebiliriz. Bu fonksiyon standart bir C fonksiyon olduğu için sadece UNIX/Linux türevi işletim sistemlerinde değil, bütün sistemlerde işlev görmektedir. Dolayısıyla ilgili sistemde bulunan "shell" programını çalıştırmaktadır. Örneğin, Windows sistemlerinde "cmd.exe" programını çalıştırmaktadır. Pekiyi işletim sistemi olmayan sistemlerde bu fonksiyon nasıl işlev görecektir? Bu durumda bir sistemde "shell" programına benzer bir program olup olmadığını anlamak için, bu programa "NULL" argümanını geçerek sorgulama yaptırmalıyız. Eğer geri dönüş değeri "0" ise ilgili programlardan olmadığını, "non-zero" ise böyle bir "shell" programının varlığından emin olmuş oluruz. Tabii Windows, Linux, macOS gibi işletim sistemi için yazılan programlarda böylesine bir kontrole lüzum yoktur. Pekiyi bu fonksiyon başka hangi değerler ile geri dönmektedir? Daha önce de bahsedildiği üzere bu fonksiyon bünyesinde sırasıyla "fork" ve "exec" yapmaktadır. Eğer başarılı bir şekilde "fork" yapsa fakat "exec" sırasında başarısız olursa, "_exit(127)" çağrısı ile oluşturulan ve "waitpid" fonksiyonu ile elde edilen değere geri dönmektedir. Eğer "fork" sırasında ya da "fork" sonrasındaki "wait" çağrısı sırasında başarısız olursa, "-1" ile geri dönmektedir. Eğer başarılı bir şekilde "fork" ve "exec" yapmışsa, bu durumda çalıştırdığı kabuk programının "waitpid" fonksiyonuyla elde edilen "status" değerine geri dönmektedir. Örneğin, system("ls -l); şeklinde bir fonksiyon çağrısı yapalım. Bu durumda bizim prosesimiz "fork/exec" yaparak "/bin/sh" dosyasını "-c" seçeneği ile çalıştıracaktır. Hayata gelen "/bin/sh" da yine "fork/exec" yaparak, "ls" programını "-l" seçeneğiyle birlikte çalıştıracaktır. Fakat bazı "shell" programları, ikinci defa "fork" yapmadan direkt olarak "exec" yapmaktadırlar. Böylesi bir durumda ise bir defa "fork", iki defa "exec" uygulanmış olur. Eğer bu "ls" komutu da başarısız olursa, "waitpid" ile elde edilen "status" değerine, "system" fonksiyonu geri döner. UNIX/Linux sistemlerinde ise başarılı olan "shell" komutları ekseriyetle "exit code" olarak "0" ile geri dönmektedir. Fakat "errno" değişkeni sadece "fork" işlemi ve "wait" işlemi başarısız olduğunda uygun değere çekilmektedir. Dolayısıyla "errno" değerini ekrana yazdırmak için "system" fonksiyonunun geri dönüş değerinin "-1" ya da "127" olması gerekmektedir. Kabuk komutunun başarısız olması "system" fonksiyonunu bağlamaz. Aşağıda bu fonksiyonun kullanımına dair bir takım örnekler verilmiştir: * Örnek 1, #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14056 14056 16080 Feb 25 19:15 a.out -rwxrwxrwx 1 root root 372 Feb 25 19:15 main.c */ int result; result = system("ls -l"); /* * Burada "system" fonksiyonu ya "fork" ya da "fork" sonrası çağrılan * "wait" sırasında hata olması durumunda "-1" ile geri dönecektir. * Ek olarak, çalıştırdığı komutun hata vermesi durumunda, bu komutun * "waitpid" ile elde edilen "status" bilgisi sorgulanmıştır. Dolayısıyla * hata sorgulamalarını detaylı bir biçimde yapmak istiyorsak, aşağıdaki * gibi bir sorgulama yapmalıyız. */ if( result == -1 || (WIFEXITED(result) && WEXITSTATUS(result) == 127)) { fprintf(stderr, "Command failed!..\n"); exit(EXIT_FAILURE); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14033 14033 16080 Feb 25 19:16 a.out -rwxrwxrwx 1 root root 514 Feb 25 19:16 main.c 30 68 514 main.c */ int result; result = system("ls -l; wc main.c"); /* * Eğer çalıştırdığımız komutun hata durumunu sorgulamak istemiyorsak, * aşağıdaki gibi bir sorgulama da yapabiliriz. */ if( result == -1 ) { exit_sys("system"); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Temsili bir "system" fonksiyon implementasyonu(bazı detaylar göz ardı edilmiştir): #include #include #include #include int my_system(const char* command); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # /bin/sh: 1: la: not found Command failed!.. */ int result; result = my_system("la -l"); /* * Burada "fork" ya da "wait" sırasında bir hata varsa ya da başarılı * bir şekilde sonlanmış ve "exit code" bilgisi de 127 ise programın * akışı bloğun içerisine girecektir. Fakat "errno" değişkeni böylesi * bir senaryoda yeni değerine çekilmeyebilir. */ if(result == -1 || WIFEXITED(result) && WEXITSTATUS(result) == 127) { fprintf(stderr, "Command failed!..\n"); exit(EXIT_FAILURE); } return 0; } int my_system(const char* command) { /* * Eğer ilgili sistemde bir "shell" programı * yoksa "0" ile geri dönmektedir. */ if(command == NULL) return 0; pid_t pid; /* * Eğer "fork" ya da "wait" sırasında hata olursa, * "-1" ile geri dönmektedir. */ if((pid = fork()) == -1) return -1; /* * Eğer "exec" sırasında hata olursa, "127" ile * geri dönmektedir. */ if(pid == 0 && execl("/bin/sh", "/bin/sh", "-c", command, (char*)0) == -1) _exit(127); int status; /* * Eğer "fork" ya da "wait" sırasında hata olursa, * "-1" ile geri dönmektedir. */ if(waitpid(pid, &status, 0) == -1) return -1; /* * Aksi halde "wait" fonksiyonu ile elde edilen * "status" bilgisi ile geri dönmektedir. */ else return status; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi bizler ne zaman "system" fonksiyonu kullanmalı, ne zaman bizzat "fork/exec" yapmalıyız? Eğer işimizi "system" fonksiyonu görüyorsa kullanmalı, bir takım detaylar ile uğraşmak istiyorsak bizler "fork/exec" yapmalıyız. Fakat unutulmamalıdır ki "system" fonksiyonu da bünyesinde "shell" programını çalıştırdığı için bir miktar yavaş ve tükettiği kaynak açısından verimsiz olabilir. > "set-user-id", "set-group-id" ve "sticky" bayrakları: Daha önce detaylarıyla gördüğümüz dosyalara erişim hakları şunlardan meydana gelmekteydi; rwxrwxrwx Bu haklardan soldaki üçlü "owner", ortadaki üçlü "group" ve geri kalan üçlü ise "other" olarak anılan haklardı. Bu haklara ek olarak üç bayrak daha görmüştük ki bunlar hakkında detaylı bilgi o gün verilmemişti. Bahsi geçen o üç bayrak, işte konu başlığı olan bayraklardır. Dolayısıyla dosyalara erişim hakları 12 bayraktan meydana gelmektedir. Pekiyi bu bayrakları fonksiyonlarda nasıl kullanacağız? Bahse konu olan bu üç bayrak "S_ISUID", "S_ISGID" ve "S_ISVTX" sembolik sabitleridir. Bunları gerek "open", gerek "chmod" fonksiyonlarında kullanarak bir dosyanın bu haklarını değiştirebiliriz. Pekiyi "shell" programı üzerinden bu bayrakları nasıl "set" edebiliriz? "set-user-id" bayrağını bir dosya için "set" etmek için "u+s" seçeneğini "chmod" komutuyla birlikte kullanmamız gerekmektedir. Örneğin, rwxrwxrwx haklarına sahip bir "sample" dosyası düşünelim. Bu dosyanın "set-user-id" bayrağını chmod u+s sample komutu ile "set" edersek, artık yeni haklar aşağıdaki gibi olacaktır; rwsrwxrwx Buradan hareketle diyebiliriz ki eğer bir dosya daha önce "x" hakkına sahipse ve bunun üzerine "set-user-id" hakkı verilmişse "x" yerine "s" gelecektir. Unutulmamalıdır ki "u+s" diyerek bizler "owner" gruptakiler için "set-user-id" hakkını vermiş oluyoruz. Pekiyi aşağıdaki haklara sahip bir "test.txt" dosyasına "set-user-id" hakkını verirsek ne olacaktı? rw-rw-rw- Bu durumda "owner" kısmındaki "-" yerine "S" gelecektir. Yani, chmod u+s test.txt komutu sonrasında yeni haklar aşağıdaki gibi olacaktı; rwSrw-rw- Buradan hareketle diyebiliriz ki "x" hakkının daha önce verilmesine bağlı olarak son durum "s" ya da "S" olacaktır. Böylelikle bu haklara bakan kişi hem "x" hakkını hem de "set-user-id" bayrağını anlamış olacaktır. Eğer bir dosyanın "set-group-id" bayrağını "set" etmek istiyorsak, tıpkı "set-user-id" bayrağını "set" ederkenki gibi, "u+s" yerine "g+s" seçeneğini kullanmalıyız. Örneğin, chmod g+s test.txt komutunu çalıştırdığımız zaman "test.txt" dosyasının "group" kısmı için "set-group-id" bayrağı "set" edilmiş olacaktır. Tabii "group" kısmındaki "x" hakkının olmasına ya da olmamasına bağlı olarak, bu harfin olduğu yerde "s" ya da "S" harfi gelecektir. Eğer "chmod" komutuyla birlikte sadece "+s" seçeneğini kullanırsak hem "set-user-id" hem de "set-group-id" bayrakları o dosya için "set" edilmiş olacaktır. Tabii bu iki kısımdaki "x" hakkının durumuna göre "s" ya da "S" harfleri gelecektir. Son olarak "sticky" bayrağını "set" etmek için "chmod" komutuyla birlikte "+t" seçeneceğini kullanmalıyız. Çünkü bu bayrak sadece "other" kısımdakiler için mevcut. "owner" ve "group" için bu bayrağı "set" etmemiz mümkün değildir. Yine bu bayrak için "t" ya da "T" olma durumu, "x" hakkının var olup olmamasına bağlıdır. Bu üç bayrağı tekrar "unset" etmek için de "+" yerine "-" yazmalıyız. Eğer bu üç bayrağı da programlama yoluyla değiştirmek istiyorsak, "chmod" fonksiyonunu şu aşağıdaki şekilde çağırabiliriz; if(chmod("sample", S_IRWXU |S_IRWXG | S_IRWXO | S_ISUID) == -1) exit_sys("chmod"); Artık "sample" programı şu aşağıdaki haklara sahip olacaktır; rwsrwxrwx Pekiyi halihazırda var olan bir dosyaya bu üç bayrak hakkını nasıl ekleyebiliriz? Bunun için ilk önce "stat" fonksiyonlarından birisiyle ilgili dosyanın erişim haklarını temin etmeli, daha sonra bu erişim haklarıyla "S_ISUID", "S_ISGID", "S_ISVTX" bayraklarından bir ya da birkaçı ile "Bit-wise OR" işlemi yapmalıyız. Ancak "stat" fonksiyonları sadece erişim haklarını değil, ilgili dosyanın tür bilgisini de içermelidir. Dolayısıyla bu tür bilgisi bayraklarını maskeleyerek "Bit-wise OR" işlemi yapmamız tavsiye edilen bir davranıştır. Burada kullanacağımız maske ise "S_IFMT" maskesidir. Örneğin, aşağıdaki kod parçacığı sadece "set-user-id" bayrağını "sample" dosyası nezdinde "set" edecektir. struct stat finfo; if(stat("sample", &finfo) == 0) exit_sys("stat"); if(chmod("sample", (finfo.st_mode & ~S_IFMT) | S_ISUID ) == -1)) exit_sys("chmod"); Şimdi gelelim bu bayrakların asıl işlevine; "set-user-id" ve "set-group-id" bayrakları çalıştırılabilir dosyalar üzerinde etkisi olan bayraklardır. Diyelim ki bizim prosesimizin Kullanıcı ID değerleri "kaan" ve Group ID değerleri de "study" olsun. Öte yandan çalıştırmak istediğimiz "/bin/passwd" dosyasının özellikleri aşağıdaki gibi olsun; -rwsr-xr-x 1 root root ... Bütün herkesin bu dosyayı çalıştırabileceğini buradan anlayabiliyoruz. Ek olarak, "set-user-id" bayrağı da "set" edilmiştir. Bu programı çalıştırmak isteyen bizim prosesimizin Kullanıcı ID değerleri "kaan", Group ID değerleri de "study" olsun. Şimdi bizler "/bin/passwd" dosyasını çalıştırmak istediğimizde, prosesimizin Etkin Kullanıcı ID değerimiz "root" oluyor. Yani çalıştırmak istediğimiz dosyanın User ID değerine çekiliyor. Bu örnek nezdinde sanki "root" kullanıcısı çalıştırmış gibi oluyor. Bunun gerçekleşme sebebi ise ilgili "/bin/passwd" dosyasının "set-user-id" bayrağının "set" edilmesinden dolayıdır. Buradaki kilit nokta çalıştırılabilen bir dosyanın "set-user-id" bayrağı "set" edilmişse, bu dosyayı çalıştırmak isteyen prosesin Etkin Kullanıcı ID değeri iş bu dosyanın User ID değerine çekiliyor. "set-group-id" de tamamen aynı mantıkla işlev görmektedir. Yani çalıştırılabilir bir dosyanın "set-group-id" bayrağı "set" edilmişse, bu dosyayı çalıştırmak isteyen prosesin Etkin Group ID değeri, bu dosyanın Group ID değerine çekilecektir. Tabii bu bayrak, "set-user-id" bayrağı kadar önemli değildir. * Örnek 1, Aşağıdaki örnekte "main" programı, "mample" programını çalıştırmaktadır. Fakat çalıştırmadan evvel "mample" programının Kullanıcı ID ve Group ID değerleri "root" olarak değiştirilmiştir. "set-user-id" komutunu "set" etmeden ve "set" ettikten sonra "mample" programını çalıştırarak aradaki farkı görebiliriz. /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid == 0 && execl("mample", "mample", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Real User ID : 14008(runner8) Effective User ID: 14008(runner8) Real Group ID : 14008(runner8) Effective Group ID: 14008(runner8) */ struct passwd* pwd; if ((pwd = getpwuid(getuid())) == NULL) exit_sys("getpwuid"); printf("Real User ID : %lld(%s)\n", (long long)getuid(), pwd->pw_name); if ((pwd = getpwuid(geteuid())) == NULL) exit_sys("getpwuid"); printf("Effective User ID: %lld(%s)\n", (long long)geteuid(), pwd->pw_name); struct group* gr; if ((gr = getgrgid(getgid())) == NULL) exit_sys("getgrgid"); printf("Real Group ID : %lld(%s)\n", (long long)getgid(), gr->gr_name); if ((gr = getgrgid(getegid())) == NULL) exit_sys("getgrgid"); printf("Effective Group ID: %lld(%s)\n", (long long)getegid(), gr->gr_name); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Aşağıda ise bu kodları çalıştıran "shell" programının komutları sırayla yazılmıştır; gcc main.c -o main gcc mample.c -o mample ls -l mample -rwxr-xr-x 1 kaan study ... mample sudo chown root:root mample ls -l mample -rwxr-xr-x 1 root root ... mample ./main Real User ID : 14008(runner8) Effective User ID : 14008(runner8) Real Group ID : 14008(runner8) Effective Group ID: 14008(runner8) sudo chmod u+s mample ls -l mample -rwsr-xr-x 1 root root ... mample ./main Real User ID : 14008(runner8) Effective User ID : 0(root) Real Group ID : 14008(runner8) Effective Group ID: 14008(runner8) sudo chmod g+s mample ls -l mample -rwsr-sr-x 1 root root ... mample ./main Real User ID : 14008(runner8) Effective User ID : 0(root) Real Group ID : 14008(runner8) Effective Group ID: 0(root) Pekiyi "sticky" bayrağı ne işe yaramaktadır? Çok da etkisi olan bir "bit" değildir. Evvelce başka anlamlar için oluşturulmuş, fakat daha sonra başka anlamlar yüklenmiştir. Bu bayrak ise çalıştırılabilir dosyalardan ziyade dizinler üzerinde etkili olan bir bayraktır. Bir dizinin "sticky" bayrağı "set" edilirse, o dizine yazma hakkı olan proses(ler) o dizindeki başkalarına ait (yani Kullanıcı ID değeri başka olan) dosyaları SİLEMEMEKTE ve İSMİ DEĞİŞTİRİLEMEMEKTEDİR. Normalde bir dosyayı silebimek için bu dosyanın içinde bulunduğu dizinde yazma hakkımızın olması yeterlidir fakat bu bayrak "set" edilmişse artık silemiyoruz. Dolayısıyla sadece "root" prosesler ve ilgili dosyanın sahibi olan prosesler silme, isim değiştirme işlemi yapabilecektir. Örneğin, Linux sistemlerindeki "/tmp" dizininin "stick" bayrağı "set" edilmiştir. Dolayısıyla bu dizin içerisinde bulunan dosyalardan sadece bize ait olanları silebilir/ismini değiştirebiliriz. Aşağıda buna bir örnek de verilmiştir. CMD:> ls -ld /tmp drwxrwxrwt 4 root root 120 Mar 5 16:57 /tmp CMD:> id uid=14076 gid=14076 groups=14076 Daha önce de belirttiğimiz üzere "set-user-id" ve "set-group-id" bayrakları çalıştırılabilir dosyalar için söz konusudur. Fakat dizinler için "set-group-id" bayrağının bir anlamı daha vardır; bir proses, bir dizin içerisinde yeni bir dosya/dizin hayata getirirse, bu dosyanın/dizinin Kullanıcı ID değeri prosesin Etkin Kullanıcı ID değerini alır. Dosyanın/dizinin Group ID değeri için POSIX iki seçenek sunmaktadır; ya bu dosyayı/dizini hayata getiren prosesin Etkin Group ID ya da dosyanın/dizinin hayata geldiği dizinin Group ID değerini alacak. Linux sistemleri, dosyanın/dizinin hayata geldiği dizinin "set-group-id" bayrağı "set" EDİLMEMİŞSE birinci yöntemi, "set" EDİLMİŞSE ikinci yöntemi izlemektedir. BSD sistemlerinde ise varsayılan durumda ikinci yöntemi izlemektedir. Öte yandan dizinlerin "set-user-id" bayraklarının "set" edilmesi benzer bir etkiye YOL AÇMAMAKTADIR. Fakat bu iki bayrak özünde "executable" dosyalar için söz konusudur. Öte yandan "shebang" içeren dosyalarda durum nasıldır? Çünkü bunlar da neticede çalıştırılabilen birer dosyalardır. Açıkça söylemek gerekirse "exec" fonksiyonları, bir takım güvenlik açıklarından dolayı, bu bayrakları dikkate almamaktadır. Şimdi, elimizde aşağıdaki özelliklere haiz "test.script" isminde bir dosyamız olsun; rwsrwxrwx İş bu dosyamız ise bünyesinde şöyle bir "shebang" satırı barındırsın; #! /bin/abbccc Daha sonra da "other.script" isimli ve "test.script" dosyasına sembolik bağlantı içeren başka bir dosyamız olsun ve bizler bu sembolik bağlantı dosyasını "exec" yapmaya çalışalım. Bu durumda da aslında "test.script" dosyasına "exec" uygulanmış olacaktır. Dolayısıyla aslında "/bin/abbccc" dosyası çalıştırılacaktır ve komut satırı argümanı olarak da "other.script" geçilecektir. İşte tam da bu komut satırının geçildiği anda, kullanıcı "other.script" içerisindeki sembolik linki başka bir dosyaya yönlendirirse, artık "test.script" dosyası değil yeni yönlendirilen dosya çalıştırılacaktır. Bu yöntemle bizler istediğimiz programları çalıştırabiliriz. Bu durum da güvenlik açığına neden olabilir. Artık bu tip "shebang" içeren dosyaların "set-user-id" ve "set-group-id" bayrakları dikkate alınmamaktadır. > Hatırlatıcı Notlar: >> "/bin/sh" dosyası aslında "/bin/dash" dosyasına sembolik linktir. "/bin/dash" ise evvelki sistemlerde kullanılan bir "shell" programıyken, "/bin/bash" ise daha gelişmiş bir "shell" programıdır. >> "ELF" formatı bir "executable" dosya formatıdır. UNIX/Linux dünyasında en çok kullanılan dosya formatıdır. Windows ise "PE" isimli bir dosya formatı kullanmaktadır. Sırasıyla "readelf" ve "dumbbin" isimli programlar bu dosya formatlarını okumamıza olanak sağlamaktadır. >> Dosyalarda "Close-on-exec" bayrakları: Öyle bir bayraktır ki "exec" yapıldığında ilgili açık olan dosyanın "close" edileceğini belirtir ve her bir "fd" için bir bayrak vardır. Bu bayraklar prosesin kontrol bloğu içerisinde yer alır. Bu bayrağın "set" edilmesi demek, "exec" işlemi sırasında ilgili dosyaların da otomatik olarak kapatılacağı demektir. "file" nesneleri yok edilmiyor, sadece dosyalar kapatılıyor. Yani "file" içerisindeki sayaçlar bir azaltılıyor. Bu sayaçlar sıfıra düştüğünde yok ediliyor. Varsayılan durumda bu bayrak "set" EDİLMEMİŞTİR. Pekiyi bizler bu bayrağı nasıl "set" ederiz? Birinci yöntem dosyayı ilk açışımız sırasında özel bir bayrak kullanmamız ki bunun adı "FD_CLOEXEC" biçimindedir. İkinci yöntemde ise "fcntl" isimli fonksiyonu çağırmamız.