gcc-10.0.1 Określony błąd segmentu


23

Mam pakiet R z kompilowanym kodem C, który był stosunkowo stabilny od dłuższego czasu i jest często testowany na wielu różnych platformach i kompilatorach (windows / osx / debian / fedora gcc / clang).

Niedawno dodano nową platformę do ponownego przetestowania pakietu:

Logs from checks with gcc trunk aka 10.0.1 compiled from source
on Fedora 30. (For some archived packages, 10.0.0.)

x86_64 Fedora 30 Linux

FFLAGS="-g -O2 -mtune=native -Wall -fallow-argument-mismatch"
CFLAGS="-g -O2 -Wall -pedantic -mtune=native -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection"
CXXFLAGS="-g -O2 -Wall -pedantic -mtune=native -Wno-ignored-attributes -Wno-deprecated-declarations -Wno-parentheses -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection"

W tym momencie skompilowany kod natychmiast zaczął segfaultować według następujących linii:

 *** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'

Byłem w stanie konsekwentnie odtwarzać segfault przy użyciu rocker/r-basekontenera dokowanego gcc-10.0.1z poziomem optymalizacji -O2. Uruchomienie niższej optymalizacji pozbywa się problemu. Uruchamianie innych konfiguracji, w tym w ramach valgrind (zarówno -O0, jak i -O2), UBSAN (gcc / clang), nie wykazuje żadnych problemów. Jestem również dość pewien, że to się skończyło gcc-10.0.0, ale nie mam danych.

Uruchomiłem gcc-10.0.1 -O2wersję gdbi zauważyłem coś, co wydaje mi się dziwne:

gdb vs kod

Podczas przechodzenia przez podświetloną sekcję wydaje się, że inicjalizacja drugich elementów tablic jest pomijana ( R_allocjest to opakowanie, mallocktóre gromadzi samo śmieci, gdy wraca kontrola do R; segfault zdarza się przed powrotem do R). Później program ulega awarii, gdy uzyskiwany jest dostęp do niezainicjowanego elementu (w wersji gcc.10.0.1 -O2).

Naprawiłem to, jawnie inicjując dany element wszędzie w kodzie, co ostatecznie doprowadziło do jego użycia, ale tak naprawdę powinien był zostać zainicjowany na pusty ciąg, a przynajmniej tak bym się spodziewał.

Czy brakuje mi czegoś oczywistego lub robię coś głupiego? Oba są dość prawdopodobne, ponieważ C jest zdecydowanie moim drugim językiem . Dziwne, że to właśnie się pojawiło i nie mogę zrozumieć, co kompilator próbuje zrobić.


UPDATE : Instrukcje do odtworzenia tego, choć będzie to jedynie odtworzyć tak długo, jak debian:testingpojemnik doker ma gcc-10co gcc-10.0.1. Ponadto, nie wystarczy uruchomić te polecenia, jeśli nie ufasz mi .

Niestety nie jest to minimalny odtwarzalny przykład.

docker pull rocker/r-base
docker run --rm -ti --security-opt seccomp=unconfined \
  rocker/r-base /bin/bash
apt-get update
apt-get install gcc-10 gdb
gcc-10 --version  # confirm 10.0.1
# gcc-10 (Debian 10-20200222-1) 10.0.1 20200222 (experimental) 
# [master revision 01af7e0a0c2:487fe13f218:e99b18cf7101f205bfdd9f0f29ed51caaec52779]

mkdir ~/.R
touch ~/.R/Makevars
echo "CC = gcc-10
CFLAGS = -g -O2 -Wall -pedantic -mtune=native -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection
" >> ~/.R/Makevars

R -d gdb --vanilla

Następnie w konsoli R, po wpisaniu runaby gdburuchomić program:

f.dl <- tempfile()
f.uz <- tempfile()

github.url <- 'https://github.com/brodieG/vetr/archive/v0.2.8.zip'

download.file(github.url, f.dl)
unzip(f.dl, exdir=f.uz)
install.packages(
  file.path(f.uz, 'vetr-0.2.8'), repos=NULL,
  INSTALL_opts="--install-tests", type='source'
)
# minimal set of commands to segfault
library(vetr)
alike(pairlist(a=1, b="character"), pairlist(a=1, b=letters))
alike(pairlist(1, "character"), pairlist(1, letters))
alike(NULL, 1:3)                  # not a wild card at top level
alike(list(NULL), list(1:3))      # but yes when nested
alike(list(NULL, NULL), list(list(list(1, 2, 3)), 1:25))
alike(list(NULL), list(1, 2))
alike(list(), list(1, 2))
alike(matrix(integer(), ncol=7), matrix(1:21, nrow=3))
alike(matrix(character(), nrow=3), matrix(1:21, nrow=3))
alike(
  matrix(integer(), ncol=3, dimnames=list(NULL, c("R", "G", "B"))),
  matrix(1:21, ncol=3, dimnames=list(NULL, c("R", "G", "B")))
)

# Adding tests from docs

mx.tpl <- matrix(
  integer(), ncol=3, dimnames=list(row.id=NULL, c("R", "G", "B"))
)
mx.cur <- matrix(
  sample(0:255, 12), ncol=3, dimnames=list(row.id=1:4, rgb=c("R", "G", "B"))
)
mx.cur2 <-
  matrix(sample(0:255, 12), ncol=3, dimnames=list(1:4, c("R", "G", "B")))

alike(mx.tpl, mx.cur2)

Sprawdzanie w gdb dość szybko pokazuje (o ile dobrze rozumiem), że CSR_strmlen_xpróbuje uzyskać dostęp do łańcucha, który nie został zainicjowany.

AKTUALIZACJA 2 : jest to wysoce rekurencyjna funkcja, a ponadto bit inicjujący ciąg jest wywoływany wiele, wiele razy. Jest to głównie b / c byłem leniwy, potrzebujemy tylko zainicjowanych łańcuchów, gdy napotkamy coś, co chcemy zgłosić w rekurencji, ale łatwiej było zainicjalizować za każdym razem, gdy można coś napotkać. Wspominam o tym, ponieważ to, co zobaczysz później, pokazuje wiele inicjalizacji, ale używana jest tylko jedna z nich (prawdopodobnie ta o adresie <0x1400000001>).

Nie mogę zagwarantować, że rzeczy, które tu pokazuję, są bezpośrednio związane z elementem, który spowodował segfault (chociaż jest to ten sam nielegalny dostęp do adresu), ale jak zapytał @ nate-eldredge, pokazuje, że element tablicy nie jest zainicjowany albo tuż przed powrotem, albo tuż po powrocie w funkcji wywoływania. Zauważ, że funkcja wywołująca inicjuje 8 z nich, i pokazuję je wszystkie, wszystkie wypełnione śmieciami lub niedostępną pamięcią.

wprowadź opis zdjęcia tutaj

AKTUALIZACJA 3 , demontaż danej funkcji:

Breakpoint 1, ALIKEC_res_strings_init () at alike.c:75
75    return res;
(gdb) p res.current[0]
$1 = 0x7ffff46a0aa5 "%s%s%s%s"
(gdb) p res.current[1]
$2 = 0x1400000001 <error: Cannot access memory at address 0x1400000001>
(gdb) disas /m ALIKEC_res_strings_init
Dump of assembler code for function ALIKEC_res_strings_init:
53  struct ALIKEC_res_strings ALIKEC_res_strings_init() {
   0x00007ffff4687fc0 <+0>: endbr64 

54    struct ALIKEC_res_strings res;

55  
56    res.target = (const char **) R_alloc(5, sizeof(const char *));
   0x00007ffff4687fc4 <+4>: push   %r12
   0x00007ffff4687fc6 <+6>: mov    $0x8,%esi
   0x00007ffff4687fcb <+11>:    mov    %rdi,%r12
   0x00007ffff4687fce <+14>:    push   %rbx
   0x00007ffff4687fcf <+15>:    mov    $0x5,%edi
   0x00007ffff4687fd4 <+20>:    sub    $0x8,%rsp
   0x00007ffff4687fd8 <+24>:    callq  0x7ffff4687180 <R_alloc@plt>
   0x00007ffff4687fdd <+29>:    mov    $0x8,%esi
   0x00007ffff4687fe2 <+34>:    mov    $0x5,%edi
   0x00007ffff4687fe7 <+39>:    mov    %rax,%rbx

57    res.current = (const char **) R_alloc(5, sizeof(const char *));
   0x00007ffff4687fea <+42>:    callq  0x7ffff4687180 <R_alloc@plt>

58  
59    res.target[0] = "%s%s%s%s";
   0x00007ffff4687fef <+47>:    lea    0x1764a(%rip),%rdx        # 0x7ffff469f640
   0x00007ffff4687ff6 <+54>:    lea    0x18aa8(%rip),%rcx        # 0x7ffff46a0aa5
   0x00007ffff4687ffd <+61>:    mov    %rcx,(%rbx)

60    res.target[1] = "";

61    res.target[2] = "";
   0x00007ffff4688000 <+64>:    mov    %rdx,0x10(%rbx)

62    res.target[3] = "";
   0x00007ffff4688004 <+68>:    mov    %rdx,0x18(%rbx)

63    res.target[4] = "";
   0x00007ffff4688008 <+72>:    mov    %rdx,0x20(%rbx)

64  
65    res.tar_pre = "be";

66  
67    res.current[0] = "%s%s%s%s";
   0x00007ffff468800c <+76>:    mov    %rax,0x8(%r12)
   0x00007ffff4688011 <+81>:    mov    %rcx,(%rax)

68    res.current[1] = "";

69    res.current[2] = "";
   0x00007ffff4688014 <+84>:    mov    %rdx,0x10(%rax)

70    res.current[3] = "";
   0x00007ffff4688018 <+88>:    mov    %rdx,0x18(%rax)

71    res.current[4] = "";
   0x00007ffff468801c <+92>:    mov    %rdx,0x20(%rax)

72  
73    res.cur_pre = "is";

74  
75    return res;
=> 0x00007ffff4688020 <+96>:    lea    0x14fe0(%rip),%rax        # 0x7ffff469d007
   0x00007ffff4688027 <+103>:   mov    %rax,0x10(%r12)
   0x00007ffff468802c <+108>:   lea    0x14fcd(%rip),%rax        # 0x7ffff469d000
   0x00007ffff4688033 <+115>:   mov    %rbx,(%r12)
   0x00007ffff4688037 <+119>:   mov    %rax,0x18(%r12)
   0x00007ffff468803c <+124>:   add    $0x8,%rsp
   0x00007ffff4688040 <+128>:   pop    %rbx
   0x00007ffff4688041 <+129>:   mov    %r12,%rax
   0x00007ffff4688044 <+132>:   pop    %r12
   0x00007ffff4688046 <+134>:   retq   
   0x00007ffff4688047:  nopw   0x0(%rax,%rax,1)

End of assembler dump.

AKTUALIZACJA 4 :

Tak więc próbując przeanalizować tutaj standard, jego części wydają się istotne ( projekt C11 ):

6.3.2.3 Konwersje Par7> Inne argumenty> Wskaźniki

Wskaźnik do typu obiektu można przekonwertować na wskaźnik do innego typu obiektu. Jeśli wynikowy wskaźnik nie jest poprawnie wyrównany 68) dla przywoływanego typu, zachowanie jest niezdefiniowane.
W przeciwnym razie, po ponownej konwersji wynik będzie porównywany z oryginalnym wskaźnikiem. Kiedy wskaźnik do obiektu jest konwertowany na wskaźnik do typu znaku, wynik wskazuje na najniższy adresowany bajt obiektu. Kolejne przyrosty wyniku, aż do wielkości obiektu, dają wskaźniki do pozostałych bajtów obiektu.

6.5 Wyrażenia Par6

Efektywnym typem obiektu dla dostępu do jego przechowywanej wartości jest deklarowany typ obiektu, jeśli taki istnieje. 87) Jeśli wartość jest przechowywana w obiekcie, który nie ma zadeklarowanego typu poprzez wartość o typie innym niż typ znaku, wówczas typ wartości staje się efektywnym typem obiektu dla tego dostępu i dla kolejnych wejść, które nie zmodyfikuj zapisaną wartość. Jeśli wartość jest kopiowana do obiektu bez zadeklarowanego typu przy użyciu memcpy lub memmove, lub jest kopiowana jako tablica typu znaków, to skutecznym typem zmodyfikowanego obiektu dla tego dostępu i dla kolejnych dostępów, które nie modyfikują wartości, jest efektywny typ obiektu, z którego kopiowana jest wartość, jeśli ją posiada. W przypadku wszystkich innych wejść do obiektu bez zadeklarowanego typu efektywnym typem obiektu jest po prostu typ wartości użytej do uzyskania dostępu.

87) Przydzielone obiekty nie mają zadeklarowanego typu.

IIUC R_alloczwraca przesunięcie do edytowanego mallocbloku, który gwarantuje doublewyrównanie, a rozmiar bloku po przesunięciu ma żądany rozmiar (przed przesunięciem dla danych specyficznych istnieje również alokacja). R_allocrzutuje ten wskaźnik (char *)na powrót.

Sekcja 6.2.5 Par 29

Wskaźnik do unieważnienia powinien mieć takie same wymagania dotyczące reprezentacji i wyrównania, jak wskaźnik do typu znaku. 48) Podobnie wskaźniki do kwalifikowanych lub niekwalifikowanych wersji kompatybilnych typów powinny mieć takie same wymagania dotyczące reprezentacji i wyrównania. Wszystkie wskaźniki do typów konstrukcji powinny mieć takie same wymagania dotyczące reprezentacji i wyrównania.
Wszystkie wskaźniki do typów unii mają takie same wymagania dotyczące reprezentacji i wyrównania.
Wskaźniki do innych typów nie muszą mieć takich samych wymagań dotyczących reprezentacji lub wyrównania.

48) Te same wymagania dotyczące reprezentacji i wyrównania mają na celu sugerowanie wymienności jako funkcji dla funkcji, zwracania wartości z funkcji i członków związków.

Więc pytanie brzmi „czy wolno nam przekształcenie (char *)się (const char **)i napisz do niego jako (const char **)”. Mój odczyt powyższego jest taki, że dopóki wskaźniki w systemach, w których działa kod, mają wyrównanie zgodne z doublewyrównaniem, to jest w porządku.

Czy naruszamy „ścisłe aliasing”? to znaczy:

6,5 Par 7

Dostęp do przechowywanej wartości obiektu może mieć wyłącznie wyrażenie wartości, które ma jeden z następujących typów: 88)

- typ zgodny z efektywnym typem obiektu ...

88) Celem tej listy jest określenie tych okoliczności, w których obiekt może być lub nie być aliasowany.

Więc co kompilator powinien uważać za efektywny typ obiektu wskazywanego przez res.target(lub res.current)? Prawdopodobnie zadeklarowany typ (const char **), czy jest to rzeczywiście niejednoznaczne? Wydaje mi się, że nie jest tak w tym przypadku tylko dlatego, że nie ma innej „wartości” w zakresie dostępu do tego samego obiektu.

Przyznaję, że ciężko walczę o wydobycie sensu z tych części standardu.


Jeśli nie zostało to jeszcze zbadane, warto spojrzeć na demontaż, aby zobaczyć dokładnie, co się dzieje. A także w celu porównania dezasemblacji między wersjami gcc.
kaylum

2
Nie próbowałbym zadzierać z wersją łącza GCC. Miło jest się bawić, ale z jakiegoś powodu nazywa się to bagażnikiem. Niestety, prawie niemożliwe jest stwierdzenie, co jest nie tak bez (1) posiadania kodu i dokładnej konfiguracji (2) z tą samą wersją GCC (3) w tej samej architekturze. Sugerowałbym sprawdzenie, czy to się utrzymuje, gdy 10.0.1 przechodzi z pnia do stabilnego.
Marco Bonelli

1
Jeszcze jeden komentarz: -mtune=nativeoptymalizuje pod kątem konkretnego procesora, który ma Twój komputer. Będzie to różne dla różnych testerów i może być częścią problemu. Jeśli uruchomisz kompilację -v, powinieneś być w stanie zobaczyć, która rodzina procesorów znajduje się na twoim komputerze (np. -mtune=skylakeNa moim komputerze).
Nate Eldredge

1
Nadal trudno odróżnić od uruchomień debugowania. Demontaż powinien być rozstrzygający. Nie musisz nic rozpakowywać, po prostu znajdź plik .o utworzony podczas kompilacji projektu i rozłóż go. Możesz także skorzystać z disassembleinstrukcji w gdb.
Nate Eldredge

5
W każdym razie, gratulacje, jesteś jednym z nielicznych, których problemem był błąd kompilatora.
Nate Eldredge

Odpowiedzi:


22

Podsumowanie: Wygląda na to, że jest to błąd w gcc związany z optymalizacją łańcucha. Samodzielna walizka testowa znajduje się poniżej. Początkowo istniały wątpliwości, czy kod jest poprawny, ale tak mi się wydaje.

Zgłoszono błąd jako PR 93982 . Proponowana poprawka została zatwierdzona, ale nie naprawia jej we wszystkich przypadkach, co prowadzi do kontynuacji PR 94015 ( godbolt link ).

Powinieneś być w stanie obejść błąd, kompilując się z flagą -fno-optimize-strlen.


Udało mi się zredukować twój przypadek testowy do następującego minimalnego przykładu (także na Godbolt ):

struct a {
    const char ** target;
};

char* R_alloc(void);

struct a foo(void) {
    struct a res;
    res.target = (const char **) R_alloc();
    res.target[0] = "12345678";
    res.target[1] = "";
    res.target[2] = "";
    res.target[3] = "";
    res.target[4] = "";
    return res;
}

Z gcc trunk (gcc wersja 10.0.1 20200225 (eksperymentalna)) i -O2(wszystkie inne opcje okazały się niepotrzebne), wygenerowany zestaw na amd64 wygląda następująco:

.LC0:
        .string "12345678"
.LC1:
        .string ""
foo:
        subq    $8, %rsp
        call    R_alloc
        movq    $.LC0, (%rax)
        movq    $.LC1, 16(%rax)
        movq    $.LC1, 24(%rax)
        movq    $.LC1, 32(%rax)
        addq    $8, %rsp
        ret

Masz więc res.target[1]całkowitą rację, że kompilator nie inicjuje się (zauważ wyraźny brak movq $.LC1, 8(%rax)).

Ciekawie jest grać z kodem i zobaczyć, co wpływa na „błąd”. Być może znacząco, zmiana typu zwracanego R_allocna void *powoduje, że znika i daje „poprawne” dane wyjściowe zestawu. Może mniej znaczące, ale bardziej zabawne, zmiana łańcucha "12345678"na dłuższy lub krótszy również powoduje, że znika.


Poprzednia dyskusja, teraz rozwiązana - kod jest pozornie legalny.

Mam pytanie, czy Twój kod jest rzeczywiście legalny. Fakt, że odbierasz char *zwrócony R_alloc()i przesyłasz go const char **, a następnie przechowujesz, const char *wydaje się, że może to naruszać ścisłą zasadę aliasingu , ponieważ chari const char *nie są kompatybilnymi typami. Istnieje wyjątek, który pozwala na dostęp do dowolnego obiektu jako char(w celu zaimplementowania takich rzeczy memcpy), ale jest na odwrót i najlepiej rozumiem, że nie jest to dozwolone. Sprawia, że ​​kod wywołuje niezdefiniowane zachowanie, dzięki czemu kompilator może legalnie robić, co tylko chce.

Jeśli tak jest, poprawną poprawką byłoby, aby R zmienił kod tak, aby R_alloc()zwracał void *zamiast char *. Wtedy nie byłoby problemu z aliasingiem. Niestety, ten kod jest poza twoją kontrolą i nie jest dla mnie jasne, jak możesz w ogóle korzystać z tej funkcji bez naruszania ścisłego aliasingu. Obejściem może być wstawienie zmiennej tymczasowej, np. void *tmp = R_alloc(); res.target = tmp;Która rozwiązuje problem w przypadku testowym, ale nadal nie jestem pewien, czy jest to zgodne z prawem.

Jednak nie jestem pewien tego „surowe aliasing” hipotezy, ponieważ kompilacja z -fno-strict-aliasing, który AFAIK ma make gcc umożliwiają takie konstrukty, czy nie sprawić, że problem znika!


Aktualizacja. Próbując różnych opcji, odkryłem, że albo spowoduje -fno-optimize-strlenalbo -fno-tree-forwpropwygenerowanie „poprawnego” kodu. Ponadto użycie -O1 -foptimize-strlenzwraca niepoprawny kod (ale -O1 -ftree-forwpropnie daje).

Po krótkim git bisectćwiczeniu wydaje się, że błąd został wprowadzony w zatwierdzeniu 34fcf41e30ff56155e996f5e04 .


Aktualizacja 2. Próbowałem trochę zagłębić się w źródło gcc, aby zobaczyć, czego mogę się nauczyć. (Nie twierdzę, że jestem ekspertem od kompilatorów!)

Wygląda na to, że kod tree-ssa-strlen.cma śledzić ciągi pojawiające się w programie. O ile wiem, błąd polega na tym, że patrząc na instrukcję res.target[0] = "12345678";kompilator łączy adres literału łańcucha "12345678"z samym łańcuchem. (Wydaje się, że jest to związane z tym podejrzanym kodem, który został dodany do wspomnianego wyżej zatwierdzenia, gdzie jeśli próbuje policzyć bajty „ciągu”, który jest faktycznie adresem, zamiast tego patrzy na to, na co wskazuje ten adres.)

Dlatego uważa, że oświadczenie res.target[0] = "12345678", zamiast zapisywania adresu z "12345678"adresem res.target, jest przechowywanie String się pod tym adresem, jakby były oświadczenie strcpy(res.target, "12345678"). Zwróć uwagę na to, co nastąpi, że spowoduje to, że końcowy nul będzie przechowywany pod adresem res.target+8(na tym etapie w kompilatorze wszystkie przesunięcia są w bajtach).

Teraz, kiedy kompilator patrzy res.target[1] = "", traktuje to tak, jakby to było strcpy(res.target+8, ""), 8 pochodzi od wielkości a char *. To tak, jakby po prostu przechowywał nul bajt pod adresem res.target+8. Jednak kompilator „wie”, że poprzednia instrukcja już zawierała bajt nul pod tym samym adresem! W związku z tym to stwierdzenie jest „zbędne” i można je odrzucić ( tutaj ).

To wyjaśnia, dlaczego ciąg musi mieć dokładnie 8 znaków, aby wyzwolić błąd. (Chociaż inne wielokrotności liczby 8 mogą również powodować błąd w innych sytuacjach).


Przekształcenie FWIW w inny typ wskaźnika jest udokumentowane . Nie wiem o aliasingu, aby wiedzieć, czy można go przekształcić, int*ale nie const char**.
BrodieG

Jeśli moje rozumienie ścisłego aliasingu jest prawidłowe, to obsada int *jest również nielegalna (a raczej przechowywanie inttam, gdzie jest nielegalne).
Nate Eldredge

1
Nie ma to nic wspólnego ze ścisłą zasadą aliasingu. Ścisła reguła aliasingu dotyczy dostępu do danych, które zostały już zapisane za pomocą innego uchwytu. Ponieważ przypisujesz tylko tutaj, nie dotyczy to ścisłej reguły aliasingu. Wskaźniki rzutowania są poprawne, gdy oba typy wskaźników mają takie same wymagania wyrównania, ale tutaj rzucasz char*i pracujesz na x86_64 ... Nie widzę tutaj UB, to jest błąd gcc.
KamilCuk

1
Tak i nie, @KamilCuk. W terminologii standardu „dostęp” obejmuje zarówno odczytywanie, jak i modyfikowanie wartości obiektu. Surowa zasada aliasingu mówi zatem o „przechowywaniu”. Nie ogranicza się to do operacji odczytu. Ale w przypadku obiektów bez zadeklarowanego typu jest to podważane przez fakt, że pisanie do takiego obiektu automatycznie zmienia jego efektywny typ, tak aby odpowiadał temu, co zostało zapisane. Obiekty bez zadeklarowanego typu są dokładnie tymi dynamicznie przydzielanymi (niezależnie od typu wskaźnika, za pomocą którego są one uzyskiwane), więc w rzeczywistości nie ma tutaj naruszenia SA.
John Bollinger

2
Tak, @Nate, przy tej definicji R_alloc()program jest zgodny, niezależnie od tego, w której jednostce tłumaczeniowej R_alloc()jest zdefiniowana. Jest to kompilator, który nie jest tutaj zgodny.
John Bollinger
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.