Dopiszę kilka rzeczy, bo widzę, że jest parę nie do końca poprawnych przekonań napisanych wyżej ;)
_13th_Dragon napisał(a)
Jak brak ścieżki w argv to ścieżką jest ścieżka bieżąca, czyli pod windows: ".\" pod linuxem "./" a że zwracamy bez końcowej łamanej to jest bez końcowej czyli sama kropka.
To nie jest niestety prawdziwe stwierdzenie - plik wykonywalny może być równie dobrze w dowolnym miejscu wskazanym przez zmienną środowiskową PATH. Dla przykładu załóżmy, że mamy następujący kod:
Kopiuj
#include <stdio.h>
int main(int argc, char **argv) {
int i;
printf("argc: %i\n", argc);
for(i = 0; i < argc; i++) {
printf("argv[%i]: %s\n", i, argv[i]);
}
return 0;
}
Uruchomienie z cwd i katalogu w PATH pod Windows:
Kopiuj
gynvael:haven-windows> test.exe
argc: 1
argv[0]: test.exe
gynvael:haven-windows> mkdir asdf
gynvael:haven-windows> move test.exe asdf
1 file(s) moved.
gynvael:haven-windows> set path=asdf;%path%
gynvael:haven-windows> test
argc: 1
argv[0]: test
Jak widać argv[0] nie zawiera ścieżki. Co więcej, argv[0] nie zawiera również rozszerzenia.
Ah, i jak widać wrzuciłem sobie do PATH ścieżkę relatywną - zazwyczaj powinno się wrzucać bezwzględną, ale na potrzeby eksperymentu to wystarczy.
A teraz to samo pod Ubuntu (przy czym zostawiłem nazwę exeka "a.out', ponieważ bash posiada wbudowane polecenie o nazwie test, a jeśli się nie poda ścieżki, to przez bash preferowane jest polecenie wbudowane):
Kopiuj
10:30:56 gynvael:haven-linux> gcc test.c
10:31:01 gynvael:haven-linux> ./a.out
argc: 1
argv[0]: ./a.out
10:31:03 gynvael:haven-linux> mv a.out asdf
10:31:06 gynvael:haven-linux> export PATH=asdf:$PATH
10:31:18 gynvael:haven-linux> a.out
argc: 1
argv[0]: a.out
Aby trochę sprecyzować, argv[0] niekoniecznie musi zawierać ścieżkę do (czy nawet pełną nazwę) exeka; argv[0] zawiera informacje o tym jak program został wywołany.
Kilka losowych przykładów jeszcze.
- Linki symboliczne:
Kopiuj
10:34:37 gynvael:haven-linux> ln -s ./asdf/a.out frrr
10:34:49 gynvael:haven-linux> ./frrr
argc: 1
argv[0]: ./frrr
10:34:55 gynvael:haven-linux> ls -la frrr
lrwxrwxrwx 1 gynvael gynvael 12 Mar 9 10:34 frrr -> ./asdf/a.out
Exek się oczywiście nie nazywa "frrr", ale tak został wywołany, więc taką informację Bash przekazał do procesu potomnego.
Z tego zachowania korzysta trochę poleceń pod Unixami btw, które mają jeden plik wykonywalny, ale zachowują się różnie w zależności od tego przez który link zostały wywołane (idealnym przykładem jest busybox).
- Nic-co-by-było-związane-z-czymkolwiek-sensownym w argv[0]
Pod Ubuntu zawartość argv[0] całkowicie kontroluje proces-rodzic, więc nie musi tam być absolutnie nic związanego z tym co jest wywoływane. Rozważmy następujący kod:
Kopiuj
#include <unistd.h>
int main(int argc, char **argv, char **envp) {
char * new_argv[] = {
"ala ma kota kot ma ale",
NULL
};
execve("/tmp/asdf/a.out", new_argv, envp);
return 0;
}
EDIT
Mieliśmy z @KrzaQ krótką dyskusje o tym czemu zrobiłem coś tak głupiego jak przypisanie literału tekstowego do char*
, więc krótkie wyjaśnienie: jak najbardziej zdaje sobie sprawę, że w C domyślnie literały są const i kompilator może je wrzucić do pamięci read-only (co więcej, jak @KrzaQ wskazał, kompilator może różne literały połączyć w pamięci w jeden - np. zamiast osobno wrzucać "foobar" i "bar", wrzucić tylko "foobar", bo ten zawiera "bar" w sobie). Natomiast tutaj jest jeszcze jeden czynnik - execve z losowych powodów wymaga char*
a nie const char*
, mimo, iż jest gwarancja, iż char*
nigdy nie zostanie zmodyfikowany (mowa cały czas o pojedynczym elemencie argv[]
) - vide http://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html . Ja natomiast byłem trochę leniwy i nie chciało mi się w kodzie wyżej robić rzutowania, więc od razu zrobiłem char*
- to przejdzie w przypadku eksperymentu, ale w kodzie produkcyjnym ofc lepiej tak nie robić. Przy czym, jeszcze trzy rzeczy:
- "writable string literals" jest wymienione w standardzie C11 jako "common extension", więc niektóre kompilatory mogą domyślnie robić zapisywalne literały, ew. może być opcja by takie zrobić (np. @KrzaQ wskazał -fwritable-strings z GCC; w dokumentacji jest fajny dopisek btw: "Writing into string constants is a very bad idea; "constants" should be constant.")
- może się okazać, że kompilator (przy normalnych zachowaniu) wrzuci string literal do sekcji .rdata (read-only data), która z założenia jest tylko do odczytu, ale, z uwagi na układ sekcji, .rdata trafi do pamięci mimo wszystko zapisywalnej (bardzo rzadkie, ale chyba widziałem coś takiego) albo wykonywalnej - to akurat dość częste w przypadku GCC na Ubuntu.
- nie wiem jak jest teraz, ale jeszcze parę lat temu jak się spakowało exeka UPXem, to ten zmieniał wszystkie rzeczy tylko-do-odczytu na read-write-execute, co bardzo ułatwiało exploitacje ;)
KONIEC EDIT
Kompilacja i uruchomienie:
Kopiuj
10:36:58 gynvael:haven-linux> gcc run.c
10:38:29 gynvael:haven-linux> ./a.out
argc: 1
argv[0]: ala ma kota kot ma ale
Co więcej, pod Windowsem jest dokładnie tak samo ;)
Kopiuj
#include <windows.h>
int main(void) {
PROCESS_INFORMATION pi;
STARTUPINFO si = { sizeof(STARTUPINFO) };
CreateProcess(
"asdf\\test.exe",
"ala ma kota kot ma ale",
NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
WaitForSingleObject(pi.hProcess, 1000);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
Kopiuj
gynvael:haven-windows> gcc runwin.c -o runwin
gynvael:haven-windows> runwin
argc: 6
argv[0]: ala
argv[1]: ma
argv[2]: kota
argv[3]: kot
argv[4]: ma
argv[5]: ale
Ciekawostka: A magia to się zaczyna dopiero dziać, jak się da "*" jako command line ;)
Kopiuj
gynvael:haven-windows> runwin
argc: 5
argv[0]: .runwin.c.swp
argv[1]: asdf
argv[2]: runwin.c
argv[3]: runwin.exe
argv[4]: test.c
Przy czym to akurat specyfika domyślnego zachowania prologu kompilatora MinGW, która rozwija symbole wieloznaczne (*, ?) w argv[] (łącznie z argv[0]) do listy plików spełniających określony pattern (jeśli ktoś ma moją książkę, to coś więcej napisałem o tym na stronie 340 w ramce "Argumenty main i rozwinięcie symboli wieloznacznych [VERBOSE]"; a jeśli ktoś nie ma, to szybkie info: aby to wyłączyć w MinGW trzeba w programie mieć zmienną globalną int _CRT_glob=0
, natomiast aby to włączyć w MSVC, trzea zlinkować z plikiem setargv.obj lub wsetargv.obj).
W ramach ciekawostki jeszcze dodam, że MAXPATH (~259 znaków) wystarczy w 99.999% przypadków, natomiast pod Windowsem ścieżki mogą mieć do 32K znaków (vide http://www.icewall.pl/?p=467).
Na zakończenie jeszcze info jak pobrać nazwę i ścieżkę pliku wykonywalnego pod Ubuntu:
Kopiuj
#include <stdio.h>
#include <unistd.h>
int main(void) {
char exe_name[4096] = {0};
readlink("/proc/self/exe", exe_name, sizeof(exe_name) - 1);
printf("exe: %s\n", exe_name);
return 0;
}
Kopiuj
10:58:46 gynvael:haven-linux> gcc getname.c
10:58:49 gynvael:haven-linux> ./a.out
exe: /tmp/a.out
I jeszcze jedna ciekawostka (kudos jagger) - /proc/self/exe nie do końca jest linkiem ;)
Okazuje się, że nawet jeśli usuniemy plik wykonywalny (a proces nadal będzie chodzić ofc), to po /proc/self/exe nadal dostaniemy się do pliku wykonywalnego, pomimo tego, iż ls będzie twierdzić, że docelowy plik został usunięty. Np.
Kopiuj
11:01:24 gynvael:haven-linux> cp /bin/cat /tmp/xxx
11:01:29 gynvael:haven-linux> ./xxx
^Z
[1]+ Stopped ./xxx
11:01:39 gynvael:haven-linux> rm xxx
11:01:46 gynvael:haven-linux> ls -la xxx
ls: cannot access xxx: No such file or directory
11:01:54 gynvael:haven-linux> ps aux | grep xxx | grep -v grep
gynvael 19605 0.0 0.0 7212 360 pts/13 T 11:01 0:00 ./xxx
11:02:03 gynvael:haven-linux> ls -la /proc/19605/exe
lrwxrwxrwx 1 gynvael gynvael 0 Mar 9 11:02 /proc/19605/exe -> /tmp/xxx (deleted)
11:02:14 gynvael:haven-linux> cat /proc/19605/exe | hexdump -C | head
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 02 26 40 00 00 00 00 00 |..>......&@.....|
00000020 40 00 00 00 00 00 00 00 20 b4 00 00 00 00 00 00 |@....... .......|
00000030 00 00 00 00 40 00 38 00 09 00 40 00 1c 00 1b 00 |....@.8...@.....|
00000040 06 00 00 00 05 00 00 00 40 00 00 00 00 00 00 00 |........@.......|
00000050 40 00 40 00 00 00 00 00 40 00 40 00 00 00 00 00 |@.@.....@.@.....|
00000060 f8 01 00 00 00 00 00 00 f8 01 00 00 00 00 00 00 |................|
00000070 08 00 00 00 00 00 00 00 03 00 00 00 04 00 00 00 |................|
00000080 38 02 00 00 00 00 00 00 38 02 40 00 00 00 00 00 |8.......8.@.....|
00000090 38 02 40 00 00 00 00 00 1c 00 00 00 00 00 00 00 |8.@.............|
11:02:35 gynvael:haven-linux> killall -9 xxx
[1]+ Killed ./xxx
11:02:46 gynvael:haven-linux>
Nyom.
vpiotrpath
to na 100% nie o to chodziło OP. Co do błędu - po pierwsze, dotyczy drugiej części czyli innego przypadku; po drugie, jest zagwarantowane że obszar jest ciągły - owszem nie jest zagwarantowany los zmiennych typu string z których skopiowaliśmy ten path, a że z niczego nie był kopiowany - to nie ma żadnych obaw.vpiotrvpiotr