Różnica dotyczy konwersji string->int liczb które wykraczają poza zasięg zmiennej, tj. są większe niż INT_MAX (0x7fffffff = 2147483647), lub mniejsze niż INT_MIN (0x80000000 = -2147483648). Przykładowo:
printf("%.8x\n", atoi("12345678901234567890"));
printf("%.8x\n", atoi("-12345678901234567890"));
Powyższy kod wygeneruje następujące wyniki:
* msvcrt (do wersji msvcr71 włącznie)
eb1f0ad2
14e0f52e
* msvcrt (od wersji msvcr80 włącznie)
7fffffff
80000000
* glibc
7fffffff
80000000
Czyli, jak widać powyżej, w przypadku starszych msvcrt następuje overflow, czyli to co widzimy w wyniku to 32 dolne bity większej liczby; natomiast w przypadku glibc oraz najnowszych msvcrt mamy do czynienia z saturacją, czyli konwertowana liczba nasyca się po osiągnięciu wartości granicznej (INT_MAX lub INT_MIN).
Żeby było zabawniej, zachowanie to bywa różne dla różnych funkcji (atoi/sscanf %i/strtol).
Anyway, zrobiłem kilka testów tych trzech funkcji (dla liczb podanych w przykładowym kodzie) na różnych wersjach MS C-Runtime Lib i GNU C Library, oraz na OSX, i wyniki wyszły ciekawe (szczególnie na OSX):
name | arch | ver | atoi- | atoi+ | sscanf- | sscanf+ | strtol- | strtol+ |
---|---|---|---|---|---|---|---|---|
crtdll.dll | 32-bit | ? | OF | OF | OF | OF | Sat. | Sat. |
msvcrt.dll | 32-bit | 7.0.7600.16385 | OF | OF | OF | OF | Sat. | Sat. |
msvcrt.dll | 32-bit | 7.0.3790.3959 | OF | OF | OF | OF | Sat. | Sat. |
msvcrt20.dll | 32-bit | 2.12.0.0 | OF | OF | OF | OF | Sat. | Sat. |
msvcrt40.dll | 32-bit | 6.1.7600.16385 | OF | OF | OF | OF | Sat. | Sat. |
msvcr71.dll | 32-bit | 7.10.3052.4 | OF | OF | OF | OF | Sat. | Sat. |
msvcr80.dll | 32-bit | 8.0.50727.4053 | Sat. | Sat. | OF | OF | Sat. | Sat. |
msvcr90.dll | 32-bit | 9.0.30729.1 | Sat. | Sat. | OF | OF | Sat. | Sat. |
msvcr100.dll | 32-bit | 10.0.30319.1 | Sat. | Sat. | OF | OF | Sat. | Sat. |
GNU Lib. C | 32-bit | 2.7 | Sat. | Sat. | Sat. | Sat. | Sat. | Sat. |
GNU Lib. C | 32-bit | 2.7-10 | Sat. | Sat. | Sat. | Sat. | Sat. | Sat. |
GNU Lib. C | 32-bit | 2.11.1 | Sat. | Sat. | Sat. | Sat. | Sat. | Sat. |
GNU Lib. C | 64-bit | 2.9 | -1 | 0 | -1 | 0 | -1 | 0 |
GNU Lib. C | 64-bit | 2.11.1 | -1 | 0 | -1 | 0 | -1 | 0 |
GNU Lib. C | 64-bit | 2.11.2 | -1 | 0 | -1 | 0 | -1 | 0 |
OSX ? Lib. C | 64-bit | OSX 10.6.4 | -1 | 0 | -1 | 0 | -1 | 0 |
OSX ? Lib. C | 32-bit | OSX 10.5.8 | Sat. | Sat. | -1 | 0 | Sat. | Sat. |
Podziękowania dla Zarula Shahrina oraz Unavoweda za wykonanie testów na OSX'ach.
Update: Podziękowania za wyniki oraz uwagi dla: djstrong, przemoc, ppkt, Rolek, faramir
Update: W tabeli, na 64-bitowych systemach, wynik funkcji strtol jest zrzutowany na int
Musze przyznać, że zwrócone w kilku przypadkach wyniki na OSX (-1 oraz 0) są pewnym zaskoczeniem dla mnie i jestem ciekaw z czego wynikają (jak mi się uda sprawdzić, to będzie update do postu).
Update: W przypadku 64-bitowych systemów i strtol zrzutowanej na int oraz funkcji polegających na strtol zwracane jest (kolejno) -1 oraz 0. Dzieję się tak ponieważ strtol zwraca (jak słusznie Unavowed oraz przemoc zauważyli) long int, który na 32-bitowych systemach ma 32-bity, a na 64-bitowych ma 64-bity, a więc strtol zwraca (kolejno) 0x7fffffffffffffff oraz 0x8000000000000000, które po przycięciu do 32-bitów dają (kolejno) 0xffffffff oraz 0x00000000, czyli -1 oraz 0.
Natomiast nie tłumaczy to specyficznego zachowania %i w sscanf na 32-bitowym OSX (być może pojawi się więc kolejny update).
Update 3: (ponowne podziękowania dla Zarula) Sprawa się wyjaśniła: sscanf w OSX korzysta wewnętrznie z funkcji strtoimax która zwraca intmax_t, który jest zdefiniowany na x86 jako int64_t, czyli po prostu long long. Więc konwersja jest wykonywana na 64-bitach z saturacją, a następnie zwracane są dolne 32-bity, czyli -1 lub 0.
Warto byłoby jeszcze sprawdzić czy za każdym razem w przypadku saturacji następuje ustawienie zmiennej globalnej errno na ERANGE (patrz cytaty ze standardu c99 do strtol poniżej).
Update oraz Update 2:
W komentarzu poniżej Rolek wrzucił wyniki powyższego testu również dla funkcji atoi/atol/wtoi/wtol (dla liczb 1234567890123456789012345678901234567890 oraz -1234567890123456789012345678901234567890). Jak się okazało, w przypadku crtdll.dll (wersja ?) oraz msvcrt20.dll (wersja ?) wyniki dla testowych liczb (oczywiście w przypadku wto[il] podanych w wersji wide/unicode) są zaskakująco różne od zwróconych przez ato[il]:
* crtdll.dll oraz msvcrt20.dll
atoi 0xCE3F0AD2 0x31C0F52E (OF)
atol 0xCE3F0AD2 0x31C0F52E (OF)
wtoi 0xEB1F0AD2 0x82167EEB (truncate(20) → OF)
wtol 0xEB1F0AD2 0x82167EEB (truncate(20) → OF)
Skąd ta różnica? Jak się okazuje, autorzy wto[li] w tychże wersjach poszli na łatwiznę robiąc z wto[li] wrapper na atol, który zajmuje się również konwersją wide-char→ascii (za pomocą funkcji WideCharToMultiByte z limitem wielkości ustawionym na 20 znaków (czyli max 20 cyfr w przypadku liczb dodatnich, oraz max 19 cyfr w przypadku liczb ujemnych, z uwagi na znak minus).
W nowszych wersjach bibliotek wto[li] zaimplementowane jest jako normalna funkcja (a nie konwertujący wrapper), oraz posiada wsparcie dla cyfr reprezentowanych nie tylko tak jak w ASCII (U+0030 do U+003A), ale również dla około połowy innych znaków którymi w 16-bit unicode można zapisać cyfrę (np. U+0660 do U+066A aka Arabic Digits, czy U+FF10 do U+FF1A aka Full Width Digit).
printf("%i\n", _wtoi(L"\uFF11\u0663\u0C69\u17e7")); → 1337
"\uFF11\u0663\u0C69\u17e7" to oczywiście 1٣౩៧
By the way...
If you'd like to learn SSH in depth, in the second half of January'25 we're running a 6h course - you can find the details at hexarcana.ch/workshops/ssh-course
OK, na koniec kilka cytatów ze standardu C99:
atoi
The functions atof, atoi, atol, and atoll need not affect the value of the integer
expression errno on an error. If the value of the result cannot be represented, the
behavior is undefined.
sscanf %i
Nie znalazłem wzmianki o takiej sytuacji, a jedynie pewne sugestie (nie wyrażone wprost), że ma się zachowywać podobnie do strtod. Czyli UB.
strtol
If the correct value
is outside the range of representable values, LONG_MIN, LONG_MAX, LLONG_MIN,
LLONG_MAX, ULONG_MAX, or ULLONG_MAX is returned (according to the return type
and sign of the value, if any), and the value of the macro ERANGE is stored in errno.
OK, i tyle na dziś :)
P.S. Hmmm, wie ktoś może jak sprawdzić wersję lib C na OSX?
P.S.2. Gdyby ktoś chciał odpalić test na jakiejś innej wersji/implementacji standardowej biblioteki C, to podaje kod (a wyniki, razem z wersją lib c oraz system operacyjnym (łącznie z tym czy jest on 32- czy 64-bitowy) chętnie przyjmę w komentarzach ;>):
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
puts("atoi");
printf(" %.8x\n", atoi("12345678901234567890"));
printf(" %.8x\n", atoi("-12345678901234567890"));
printf("sscanf %%i\n");
{ int res = 0; sscanf("12345678901234567890", "%i", &res); printf(" %.8x\n", res); }
{ int res = 0; sscanf("-12345678901234567890", "%i", &res); printf(" %.8x\n", res); }
printf("strtol\n");
printf(" %.8x\n", strtol("12345678901234567890", NULL, 10));
printf(" %.8x\n", strtol("-12345678901234567890", NULL, 10));
return 0;
}
P.S.3. Zachęcam do przejrzenia dodatkowych testów i programów testujących umieszczonych w komentarzach przez czytelników :)
Comments:
glibc 2.9
atoi
ffffffff
00000000
sscanf %i
ffffffff
00000000
strtol
ffffffff
00000000
Jaki OS / ilu bitowy?
Windows 7 x64 Ultimate
C:Dev-Cppin>a.exe
atoi
eb1f0ad2
14e0f52e
sscanf %i
eb1f0ad2
14e0f52e
strtol
7fffffff
80000000
mingwm10.dll nie jest implementacją std lib c :)
Ale thx za potwierdzenie wyników msvcrt.dll :)
Jaka architektura? Ok, pewnie x86. ;) Ile bitowa?
Jaki system w przypadku glibc, w sensie chociaż *nix czy win?
Sam nie podałeś, ale słusznie od innych wymagasz (no bo długość słowa w procesorze wpływa przecież na rozmiar longa). ;P
Nie to żeby algorytmy miały się jakoś różnić z powodu liczby bitów, ale sam rozumiesz...
Jeżeli już porównujemy ze strtol(), to powinniśmy raczej posługiwać się sscanf(%li) i atol().
BTW DevCpp? Toż to jeszcze funkcjonuje w środowisku? To był zawsze złom, dostarczający ponadto archaicznych wersji kompilatora MinGW/GCC (a ludzie są leniwi by samodzielnie sobie ściągać stosowne komponenty). Dla błądzących: Code::Blocks lub Qt Creator, oba działające zarówno pod Windowsem jak i Linuksem. Przy okazji zwracam uwagę na MinGW-W64 - dużo żywiej rozwijany od MinGW, wspierający jak nazwa wskazuje generację 64-bitowego kodu no i choćby niwelujący potrzebę dostarczania pliku mingwm10.dll z binarkami.
kernel 2.6.32.11, Slackware 13.0.0.0.0 x64 na Xeonie X7350
atoi
ffffffff
00000000
sscanf %i
ffffffff
00000000
strtol
ffffffff
00000000
Debian 64 bit, libc 2.11.2
Co do architektury - słuszne zastrzeżenie. Przyznaję, że nie myślałem że w przypadku typu int będzie różnica między 64-bit a 32-bit (tj. atoi oraz [fs]scanf %i)... a wygląda na to, że jest.
Co do glibc, dopiszę potem, ale hmm... przyznaję, że nie widziałem nigdy używanego glibc pod Windows (chociaż za bardzo to się np. cygwinowi nie przyglądałem). Natomiast fakt faktem niektóre systemy dodają do glibc jakieś swoje patche, więc może warto to zaznaczyć.
Co do strtol: moim wstępnym zamysłem było sprawdzenie powyższego na 32-bitowej platformie, a tam long int == int, więc bez różnicy. Natomiast z uwagi na istotne różnice w 64-bitowych std lib c, wygląda na to, że bedę musiał to wziąć pod uwagę.
Ad Dev-Cpp, nee, to nie zawsze był złom. Pamiętam czasy kiedy w Dev-Cpp była jedna z nowszych wersji gcc ;)
@djstrong
Thx, to rozjaśnia sprawę :)
@ppkt
A to ciekawe... nie spotkałem się żeby %.8x chciał unsigned int. Ciekawe, ciekawe ;)
Anyway, thx za wyniki :)
Fakt, że kiedyś z wersjami MinGW-GCC było lepiej pod DevCpp, ale po prostu to IDE jest strasznie zabugowane i głównie do tego piłem. ;) Chyba każdy kto spróbował tego poużywać dłużej niż 5-10 minut, może to potwierdzić. Oczywiście bardzo szybko i już dawno temu zakończyłem przygodę z nim, więc mogę nie być świadomym potencjalnie licznych poprawek, które miały miejsce później.
Dopiero teraz zauważyłem, że jest jeszcze małe niedopatrzenie w kodzie - brak nagłówka stdlib.h. Jeżeli ktoś kompiluje bez warningów (tzw. ściany pod gcc) to tego nie zauważy.
Mój kod (korzystający konsekwentnie z longa) użyty do testów pod GCC:
https://gist.github.com/708845
Brak inkluzji wspomnianego nagłówka ma zresztą wpływ na wyniki, bo leci wtedy wersja intowa pod libc dla atol() i strtol() dając wyniki takie jak w kompilacji 32-bit dla tych funkcji (co do wartości, nie liczby wyświetlanych znaków).
** Debian GNU/Linux testing (squeeze) x86_64 | eglibc 2.11.2-7
* kompilacja 64-bit
ATOL
7fffffffffffffff
8000000000000000
SSCANF
7fffffffffffffff
8000000000000000
STRTOL
7fffffffffffffff
8000000000000000
* kompilacja 32-bit
ATOL
7fffffff
80000000
SSCANF
7fffffff
80000000
STRTOL
7fffffff
80000000
** Slackware 12.1 | glibc 2.7-10
* kompilacja 32-bit
ATOL
7fffffff
80000000
SSCANF
7fffffff
80000000
STRTOL
7fffffff
80000000
** Windows XP Pro x64 SP2 | msvcrt 7.0.3790.3959
* kompilacja 64-bit
ATOL
eb1f0ad2
14e0f52e
SSCANF
eb1f0ad2
14e0f52e
STRTOL
7fffffff
80000000
* kompilacja 32-bit
ATOL
eb1f0ad2
14e0f52e
SSCANF
eb1f0ad2
14e0f52e
STRTOL
7fffffff
80000000
Z racji, że rzadko w windzie rzeźbie, zdążyłem nawet zapomnieć o (LL)P64, więc się chwilę nad wynikiem 64-bitowej kompilacji zastanawiałem. :)
Thx za wyniki! Mógłbyś jeszcze sprawdzić atoi() i sscanf(%i) na 64-bitowej platformie? Bo wygląda na to, że tam wyniki są bardzo ciekawe.
Co do stdlib.h, yep, masz racje (Unavowed dał mi o tym znać chwilę wcześniej).
Wrzuce dzisiaj albo jutro w post wszystkie poprawki.
Ms Visual C++ 2010 EE 32bit Ms Windows XP Pro SP3 32bit
crtdll.dll
atoi 0xCE3F0AD2 0x31C0F52E
atol 0xCE3F0AD2 0x31C0F52E
wtoi 0xEB1F0AD2 0x82167EEB
wtol 0xEB1F0AD2 0x82167EEB
strtol 0x7FFFFFFF 0x80000000
wcstol 0x7FFFFFFF 0x80000000
sscanf%i 0xCE3F0AD2 0x31C0F52E
sscanf%d 0xCE3F0AD2 0x31C0F52E
swscanf%i 0xCE3F0AD2 0x31C0F52E
swscanf%d 0xCE3F0AD2 0x31C0F52E
msvcrt.dll
atoi 0xCE3F0AD2 0x31C0F52E
atol 0xCE3F0AD2 0x31C0F52E
wtoi 0xCE3F0AD2 0x31C0F52E
wtol 0xCE3F0AD2 0x31C0F52E
strtol 0x7FFFFFFF 0x80000000
wcstol 0x7FFFFFFF 0x80000000
sscanf%i 0xCE3F0AD2 0x31C0F52E
sscanf%d 0xCE3F0AD2 0x31C0F52E
swscanf%i 0xCE3F0AD2 0x31C0F52E
swscanf%d 0xCE3F0AD2 0x31C0F52E
msvcrt20.dll
atoi 0xCE3F0AD2 0x31C0F52E
atol 0xCE3F0AD2 0x31C0F52E
wtoi 0xEB1F0AD2 0x82167EEB
wtol 0xEB1F0AD2 0x82167EEB
strtol 0x7FFFFFFF 0x80000000
wcstol 0x7FFFFFFF 0x80000000
sscanf%i 0xCE3F0AD2 0x31C0F52E
sscanf%d 0xCE3F0AD2 0x31C0F52E
swscanf%i 0xCE3F0AD2 0x31C0F52E
swscanf%d 0xCE3F0AD2 0x31C0F52E
msvcrt40.dll
atoi 0xCE3F0AD2 0x31C0F52E
atol 0xCE3F0AD2 0x31C0F52E
wtoi 0xCE3F0AD2 0x31C0F52E
wtol 0xCE3F0AD2 0x31C0F52E
strtol 0x7FFFFFFF 0x80000000
wcstol 0x7FFFFFFF 0x80000000
sscanf%i 0xCE3F0AD2 0x31C0F52E
sscanf%d 0xCE3F0AD2 0x31C0F52E
swscanf%i 0xCE3F0AD2 0x31C0F52E
swscanf%d 0xCE3F0AD2 0x31C0F52E
msvcr71.dll
atoi 0xCE3F0AD2 0x31C0F52E
atol 0xCE3F0AD2 0x31C0F52E
wtoi 0xCE3F0AD2 0x31C0F52E
wtol 0xCE3F0AD2 0x31C0F52E
strtol 0x7FFFFFFF 0x80000000
wcstol 0x7FFFFFFF 0x80000000
sscanf%i 0xCE3F0AD2 0x31C0F52E
sscanf%d 0xCE3F0AD2 0x31C0F52E
swscanf%i 0xCE3F0AD2 0x31C0F52E
swscanf%d 0xCE3F0AD2 0x31C0F52E
msvcr100.dll
atoi 0x7FFFFFFF 0x80000000
atol 0x7FFFFFFF 0x80000000
wtoi 0x7FFFFFFF 0x80000000
wtol 0x7FFFFFFF 0x80000000
strtol 0x7FFFFFFF 0x80000000
wcstol 0x7FFFFFFF 0x80000000
sscanf%i 0xCE3F0AD2 0x31C0F52E
sscanf%d 0xCE3F0AD2 0x31C0F52E
swscanf%i 0xCE3F0AD2 0x31C0F52E
swscanf%d 0xCE3F0AD2 0x31C0F52E
Hehe to mi się post na czasie udał ;)
@Rolek
Thx za wyniki, są bardzo ciekawe, szczególnie różnica między atoi a wtoi w crtdll i msvcrt20.
Btw, brakuje Ci w spisie wyników z msvcrt90 i msvcrt80. Mógłbyś je wrzucić?
(Jeśli masz kłopot z załadowaniem tych dwóch libów, to daj znać)
Zrzuty dla Windows 7 Professional (x64) z cygwina:
faramir@spire ~
$ ./a.exe
atoi
7fffffff
80000000
sscanf %i
7fffffff
80000000
strtol
7fffffff
80000000
faramir@spire ~
$ ldd.exe a.exe
ntdll.dll => /cygdrive/c/Windows/SysWOW64/ntdll.dll (0x771b0000)
kernel32.dll => /cygdrive/c/Windows/syswow64/kernel32.dll (0x76cb0000)
KERNELBASE.dll => /cygdrive/c/Windows/syswow64/KERNELBASE.dll (0x755e0000)
cygwin1.dll => /usr/bin/cygwin1.dll (0x61000000)
ADVAPI32.DLL => /cygdrive/c/Windows/syswow64/ADVAPI32.DLL (0x75040000)
msvcrt.dll => /cygdrive/c/Windows/syswow64/msvcrt.dll (0x76af0000)
sechost.dll => /cygdrive/c/Windows/SysWOW64/sechost.dll (0x75d30000)
RPCRT4.dll => /cygdrive/c/Windows/syswow64/RPCRT4.dll (0x76ba0000)
SspiCli.dll => /cygdrive/c/Windows/syswow64/SspiCli.dll (0x74d20000)
CRYPTBASE.dll => /cygdrive/c/Windows/syswow64/CRYPTBASE.dll (0x74d10000)
faramir@spire ~
$ /cygdrive/c/Program Files/SysinternalsSuite/sigcheck.exe `cygpath -w '/cygdrive/c/Windows/syswow64/msvcrt.dll'`
Sigcheck v1.70 - File version and signature viewer
Copyright (C) 2004-2010 Mark Russinovich
Sysinternals - www.sysinternals.com
c:windowssyswow64msvcrt.dll:
Verified: Signed
Signing date: 04:17 2009-07-14
Publisher: Microsoft Corporation
Description: Windows NT CRT DLL
Product: Microsoft▒ Windows▒ Operating System
Version: 7.0.7600.16385
File version: 7.0.7600.16385 (win7_rtm.090713-1255)
Nzmc:D
msvcr80 i msvcr90 nie chcą się załadować dynamicznie.
msvcr90 mogę przetestować linkując statycznie(w VC10 jest do wyboru v9.0 i v10.0) ale msvcr80 niestety nie:(
Więc niestety ale msvcr90 nie przetestuję.
Hmm... jakiej wielkości jest a.exe na cygwinie? Na pewno clib nie został zlinkowany statycznie?
@Rolek
Można je załadować dynamicznie, tylko trzeba manifesty potworzyć. Wrzuce odpowiedni setup wieczorem na serv i wrzucę tu linka.
Rzuć okiem na http://gynvael.vexillium.org/dump/msvcr8090_manifest.zip
To nadal działa. Przykład z MinGW GCC:
--kod--
#include <stdio.h>
void __stdcall WinMain(int a, int b, int c, int d) {}
int my_main(void)
{
puts ("my main");
return 0;
}
--koniec kodu--
Kompilacja:
gcc test.c -Wl,--entry=_my_main
Uwagi:
-Wl,cośtam przekazuje cośtam jako argument linkera.
W kodzie musiałem zdefiniować WinMain ponieważ nie używam -nostdlib, a jakieś tam fragmenty prologu kompilatora oczekują jednak funkcji WinMain. Gdybym jednak użył -nostdlib, to musiałbym inne funkcje podefiniować, nawet jeśli miałyby być puste i nigdy nie używane. (tak naprawdę dowolny trik który wrzuci do tablicy symboli odpowiedni symbol by tutaj zadziałał)
Ponieważ to jest Windows, to parametrem --entry jest _my_main (z dodatkowym underscorem) a nie po prostu my_main. Analogicznie by było na OSX z tego co pamiętam, natomiast pod Linux-based OSami owy dodatkowy underscore jest niepotrzebny.
msvcr80.dll
atoi 0x7FFFFFFF 0x80000000
atol 0x7FFFFFFF 0x80000000
wtoi 0x7FFFFFFF 0x80000000
wtol 0x7FFFFFFF 0x80000000
strtol 0x7FFFFFFF 0x80000000
wcstol 0x7FFFFFFF 0x80000000
sscanf%i 0xCE3F0AD2 0x31C0F52E
sscanf%d 0xCE3F0AD2 0x31C0F52E
swscanf%i 0xCE3F0AD2 0x31C0F52E
swscanf%d 0xCE3F0AD2 0x31C0F52E
msvcr90.dll
atoi 0x7FFFFFFF 0x80000000
atol 0x7FFFFFFF 0x80000000
wtoi 0x7FFFFFFF 0x80000000
wtol 0x7FFFFFFF 0x80000000
strtol 0x7FFFFFFF 0x80000000
wcstol 0x7FFFFFFF 0x80000000
sscanf%i 0xCE3F0AD2 0x31C0F52E
sscanf%d 0xCE3F0AD2 0x31C0F52E
swscanf%i 0xCE3F0AD2 0x31C0F52E
swscanf%d 0xCE3F0AD2 0x31C0F52E
Raczej żadnych niespodzianek.
A wersja crtdll.dll to 4.0.1183.1 (tak pokazuje eksplorator).
Sorry, że tak puźno ale miałem trochę ważnych spraw a potem wyleciało mi z głowy.
PS. Kiedy następny ReverseCraft? :D
Add a comment: