Czy jest możliwy uniwersalny język asemblera dla wszystkich komputerów?


23

Chciałbym zadać kilka pytań na temat języka asemblera. Rozumiem, że jest bardzo zbliżony do języka maszynowego, dzięki czemu jest szybszy i bardziej wydajny.

Skoro mamy różne architektury komputerów, czy to oznacza, że ​​muszę pisać inny kod w asemblerze dla różnych architektur? Jeśli tak, to dlaczego nie jest Zgromadzenie, napisz raz - biegnij wszędzie, językiem? Czy nie byłoby łatwiej po prostu uczynić go uniwersalnym, tak aby napisać go tylko raz i można go uruchomić na praktycznie dowolnej maszynie o różnych konfiguracjach? (Myślę, że byłoby to niemożliwe, ale chciałbym uzyskać konkretne, szczegółowe odpowiedzi)

Niektórzy ludzie mogą powiedzieć, że C jest językiem, którego szukam. Nie korzystałem wcześniej z C, ale myślę, że wciąż jest to język wysokiego poziomu, choć prawdopodobnie na przykład szybszy niż Java. Mogę się tutaj mylić.


10
Jakie badania przeprowadziłeś? Oczekujemy, że przeprowadzisz badania przed zapytaniem, aby pomóc Ci zadać lepsze pytanie. Wiele napisano na temat języka asemblera.
DW

4
Oczekujemy, że wykonasz znaczną ilość badań / samokształcenia przed zapytaniem i powiesz nam w pytaniu, jakie badania przeprowadziłeś. W takim przypadku badania mogą obejmować czytanie odpowiednich artykułów Wikipedii (np. Na temat języka asemblera i architektury komputera) oraz czytanie podręcznika architektury komputera. Aby uczynić to lepszym pytaniem: wykonaj te badania, jeśli jeszcze tego nie zrobiłeś, a następnie edytuj pytanie, aby wyjaśnić przeprowadzone badania. Często tego rodzaju badania pomagają sformułować lepsze pytanie; w każdym razie pomaga to osobom odpowiadającym uniknąć powtarzania tego, co już wiesz.
DW

15
Zacznij od zrozumienia, że ​​/ dlaczego nie ma języka o nazwie Zgromadzenie.
Raphael

2
jednym „klasycznym” problemem związanym z przenośnością C są różne pierwotne rozmiary (np. liczby całkowite) na różnych urządzeniach i jest kilka innych cytowanych.
vzn

3
Jest to bardziej problem społeczny niż techniczny - musisz przekonać wszystkich producentów procesorów, aby ich procesory zaakceptowały ten sam język maszynowy. (Właściwie x86 prawie tak miało być, przypadkiem - wtedy smartfony wystartowały)
user253751

Odpowiedzi:


45

Język asemblera to sposób pisania instrukcji dla zestawu instrukcji komputera , w sposób nieco bardziej zrozumiały dla programistów.

Różne architektury mają różne zestawy instrukcji: zestaw dozwolonych instrukcji jest różny dla każdej architektury. Dlatego nie możesz mieć nadziei, że będziesz mieć program asemblujący po uruchomieniu w dowolnym miejscu. Na przykład zestaw instrukcji obsługiwanych przez procesory x86 wygląda zupełnie inaczej niż zestaw instrukcji obsługiwanych przez procesory ARM. Jeśli napisałeś program asemblera dla procesora x86, miałby on wiele instrukcji, które nie są obsługiwane przez procesor ARM i odwrotnie.

Podstawowym powodem używania języka asemblerowego jest to, że pozwala on na bardzo niski poziom kontroli nad twoim programem i pozwala korzystać ze wszystkich instrukcji procesora: dostosowując program do korzystania z funkcji, które są unikalne dla konkretnego procesora będzie działać, czasem możesz przyspieszyć program. Filozofia „pisz raz i wszędzie” jest zasadniczo sprzeczna z tym.


1
Myślę, że na to pytanie już odpowiedział trzeci akapit mojej odpowiedzi. Jak powiedziałeś, taki schemat nie byłby skuteczny, więc byłby zasadniczo sprzeczny z głównym powodem używania języka asemblera.
DW

26
@nTuply Gdy tylko zmienisz język asemblera w celu dostosowania go do różnych maszyn, stał się on językiem wysokiego poziomu z okropnie składnią w stylu asemblera. Gdy zdecydujesz się na użycie języka wysokiego poziomu, możesz równie dobrze użyć języka o bardziej przyjaznej składni i pozwolić kompilatorowi wykonać ciężką pracę.
David Richerby,

15
Nie jest to całkowicie głupi pomysł, aby mieć „język asemblera”, który jest tłumaczony na różne maszyny, ponieważ w zasadzie taki jest „IR” LLVM. Jednak z powodów, które podaje David, zwykle nie piszesz zestawu LLVM. Również dlatego, że 99 razy na 100 napisałbyś go gorzej niż zwykłe tłumaczenie C na LLVM. Języki asemblacyjne są potencjalnie bardziej wydajne niż języki wysokiego poziomu, ale w rękach większości rzeczywistych programistów, którzy mają zwykle czas na optymalizację, i tak nie osiągają swojego potencjału.
Steve Jessop,

9
@ nTuply, że istnieje. Proces przechodzenia od tego języka asemblerowego do instrukcji maszynowych nazywa się kompilacją.
Paul Draper,

3
@PJTraill Nie ma żadnego powodu, aby pisać kompilator w asemblerze w nowoczesnym systemie, z wyjątkiem pierwszego kroku ładowania systemu (i przez większość czasu, nawet wtedy). Kompilatory napisane w języku wysokiego poziomu są znacznie łatwiejsze do utrzymania. Porównaj także W jaki sposób język, którego kompilator napisany jest w C, może być szybszy niż C? . Celem kompilatora jest tłumaczenie z jednego języka (języka źródłowego) na inny (zwykle język maszynowy dla konkretnej architektury i systemu operacyjnego); można to napisać w dowolnym języku.
CVn

13

DEFINICJA języka asemblera polega na tym, że jest to język, który można tłumaczyć bezpośrednio na kod maszynowy. Każdy kod operacji w języku asemblera przekłada się na dokładnie jedną operację na komputerze docelowym. (Cóż, jest to trochę bardziej skomplikowane: niektóre asemblery automatycznie określają „tryb adresowania” na podstawie argumentów kodu operacyjnego. Ale nadal zasada jest taka, że ​​jedna linia zestawu tłumaczy się na jedną instrukcję języka maszynowego.)

Bez wątpienia można wymyślić język, który wyglądałby jak język asemblera, ale zostałby przetłumaczony na różne kody maszynowe na różnych komputerach. Ale z definicji nie byłby to język asemblera. Byłby to język wyższego poziomu, który przypomina język asemblera.

Twoje pytanie przypomina trochę: „Czy można stworzyć łódź, która nie unosi się na wodzie lub nie ma innego sposobu na podróżowanie po wodzie, ale ma koła i silnik i może podróżować na lądzie?” Odpowiedź byłaby taka, że ​​z definicji taki pojazd nie byłby łodzią. Brzmi bardziej jak samochód.


1
C często opisywano jako „przenośny język asemblera”.
Larry Gritz,

2
@LarryGritz Sure. A kiedy wynaleziono C, było to przełomowe: oferowało dużą moc języka asemblera z łatwością użycia skompilowanego. Ale z definicji wciąż jest to skompilowany język
Jay

8

Nie ma koncepcyjne (Przypuszczam, żaden komputer nauki ) powód przeciwko posiadające jedną asemblera dla wszystkich komputerów na świecie. W rzeczywistości ułatwiłoby to wiele rzeczy. Jeśli chodzi o teorię, w każdym razie są one takie same, aż do jakiejś funky bijection.

W praktyce jednak istnieją różne układy scalone do różnych celów, z różnymi operacjami i zasadami projektowania (np. RISC vs. CISC), które służą różnym celom, a zestawy instrukcji, które je obsługują, a także języki asemblera są różne. Ostatecznie odpowiedź jest taka sama, jak w przypadku pytania, dlaczego istnieje tak wiele różnych języków programowania : różne cele, różne decyzje projektowe.

To powiedziawszy, możesz oczywiście wprowadzić poziomy abstrakcji, aby dostać się do jakiegoś wspólnego interfejsu. na przykład x86 zostało wyeliminowane na poziomie chipa od dłuższego czasu; jest mały sprzęt, który tłumaczy instrukcje x86 na wszystko, z czym procesor naprawdę współpracuje. Języki takie jak C byłyby kolejnym krokiem od sprzętu (nawet jeśli prawdopodobnie niewielkim), aż do języków takich jak Haskell, Java lub Ruby. Tak, kompilator jest jednym z głównych osiągnięć informatyki, ponieważ umożliwia rozdzielenie problemów w ten sposób.


6
„jeśli prawdopodobnie niewielki” - są tam dwa rodzaje programistów. Ci, którzy uważają C za język niskiego poziomu, ponieważ jego podstawowe operacje przypominają rzeczy, które pojawiają się w zestawach instrukcji procesora, oraz ci, którzy uważają C za język wysokiego poziomu, ponieważ nie jest to ten sam zestaw instrukcji, co maszyna.
Steve Jessop,

Jeśli przez język asemblera rozumiesz taki, który daje pełną kontrolę nad kodem maszynowym generowanym dla określonego typu (lub rodziny) sprzętu, byłoby możliwe zdefiniowanie jednego języka „dla wszystkich komputerów” w naszym świecie w danym momencie, ale byłoby muszę się zmieniać. Wprawdzie (jeśli dobrze zaprojektowane) skróciłoby to krzywą uczenia się przy kodowaniu nowej architektury, ale spodziewam się, że każde zadanie, które chciałbyś z tym zrobić, niż kompilator, dotyczyłoby tylko niewielkiej części architektur. To, że komputery są takie same na poziomie abstrakcyjnym, to czerwony śledź, chodzi o kod maszynowy.
PJTraill

7

Wspominasz wyrażenie „pisz raz, biegnij gdziekolwiek”, nie zauważając jego znaczenia. To jest slogan marketing dla Sun Microsystems , który komercyjnie wynalazł pojęcie „maszynie wirtualnej” i „bytecodes” dla Javy, choć być może pomysł mógł powstać w środowisku akademickim 1 st. Pomysł został później skopiowany przez Microsoft dla .Net po tym, jak Sun pozwał ich za naruszenie licencji Java. Bytecodes Java są implementacją idei asemblera między maszynami lub języka maszynowego. Są one używane w kilku innych językach niż Java i teoretycznie mogą być użyte do kompilacji dowolnego języka. Po wielu latach bardzo zaawansowanej optymalizacji, Java jest bliska wydajności skompilowanym językom, co pokazuje, że cel w postaci wysokowydajnej, niezależnej od platformy technologii maszyn wirtualnych jest ogólnie możliwy do osiągnięcia.

Kolejny nowy pomysł na wczesnych etapach / w obiegu związany z Twoimi wymaganiami nazywa się projektem ponownego obliczenia i jest przeznaczony do badań naukowych, chociaż można go wykorzystać do innych celów. Chodzi o to, aby eksperymenty obliczeniowe były powtarzalne za pomocą technologii maszyn wirtualnych. Jest to głównie idea symulacji różnych architektur maszyn na dowolnym sprzęcie.


8
Sun nie wynalazł ani maszyn wirtualnych, ani kodu bajtowego, nie byli nawet pierwszą grupą, która na nich zarabiała. Wyszukaj kod p.
jmoreno

@jmoreno: może także zajrzeć do Smalltalk.
Bob Jarvis - Przywróć Monikę

artykuł nie twierdzi, że Sun wynalazł maszyny wirtualne / kod bajtowy. istnieje inna historia, która nie jest cytowana, ale o której się mówi. przy okazji inna kluczowa technologia bardzo istotna tutaj: natywny klient Google (funkcja chrome)
dniu

5

Powody wysokiego poziomu

Kiedy się nad tym zastanowić, mikroprocesor robi niesamowitą rzecz: pozwala zabrać maszynę (taką jak pralka lub winda) i zastąpić cały kawałek niestandardowych mechanizmów lub obwodów tanim, masowo produkowanym krzemem żeton. Zaoszczędzisz dużo pieniędzy na częściach i dużo czasu na projekt.

Ale poczekaj, standardowy układ zastępujący niezliczone niestandardowe projekty? Nie może być jednego idealnego mikroprocesora, który byłby idealny dla każdego zastosowania. Niektóre aplikacje muszą minimalizować zużycie energii, ale nie muszą być szybkie; inne muszą być szybkie, ale nie muszą być łatwe do zaprogramowania, inne muszą być tanie, itp.

Mamy więc wiele różnych „smaków” mikroprocesora, z których każdy ma swoje mocne i słabe strony. Pożądane jest, aby wszyscy korzystali z kompatybilnego zestawu instrukcji, ponieważ umożliwia to ponowne użycie kodu i ułatwia znalezienie osób o odpowiednich umiejętnościach. Jednak zestaw instrukcji nie wpływa na koszty, złożoność, szybkość, łatwość użycia i fizyczne ograniczenia procesora, a więc mamy kompromis: istnieje kilka „mainstreamowe” zestawy instrukcji (i wiele z nich moll), a w każdym zestawie instrukcji znajduje się wiele procesorów o różnych właściwościach.

Aha, a wraz ze zmianą technologii zmieniają się wszystkie kompromisy, więc ewoluują zestawy instrukcji, pojawiają się nowe, a stare umierają. Nawet gdyby istniał dziś „najlepszy” zestaw instrukcji, może nie być za 20 lat.

Szczegóły sprzętu

Prawdopodobnie największą decyzją projektową w zestawie instrukcji jest rozmiar słowa , tzn. Jak duża liczba procesor może „naturalnie” manipulować. 8-bitowe procesory obsługują liczby od 0 do 255, podczas gdy 32-bitowe procesory obsługują liczby od 0 do 4 294 967 295. Kod zaprojektowany dla jednego musi być całkowicie przemyślany dla innego.

Nie chodzi tylko o tłumaczenie instrukcji z jednego zestawu instrukcji na inny. W innym zestawie instrukcji może być preferowane zupełnie inne podejście. Na przykład na 8-bitowym procesorze tablica odnośników może być idealna, podczas gdy na 32-bitowym procesorze lepiej byłoby wykonać operację arytmetyczną w tym samym celu.

Istnieją inne główne różnice między zestawami instrukcji. Większość instrukcji dzieli się na cztery kategorie:

  • Obliczenia (arytmetyka i logika)
  • Kontrola przepływu
  • Transfer danych
  • Konfiguracja procesora

Procesory różnią się rodzajem obliczeń, które mogą wykonywać, a także sposobem podejścia do przepływu kontroli, transferu danych i konfiguracji procesora.

Na przykład niektóre procesory AVR nie mogą się zwielokrotniać ani dzielić; podczas gdy wszystkie procesory x86 mogą. Jak można sobie wyobrazić, wyeliminowanie obwodów wymaganych do zadań takich jak mnożenie i dzielenie może uczynić procesor prostszym i tańszym; operacje te mogą być nadal wykonywane przy użyciu procedur oprogramowania, jeśli są potrzebne.

x86 pozwala instrukcjom arytmetycznym ładować operandy z pamięci i / lub zapisywać wyniki w pamięci; ARM jest architekturą przechowującą ładunki, dlatego ma tylko kilka dedykowanych instrukcji dostępu do pamięci. Tymczasem x86 ma dedykowane instrukcje rozgałęzienia warunkowego, podczas gdy ARM pozwala na warunkowe wykonanie praktycznie wszystkich instrukcji. Ponadto ARM pozwala na przeprowadzanie przesunięć bitów w ramach większości instrukcji arytmetycznych. Różnice te prowadzą do różnych charakterystyk wydajności, różnic w projekcie wewnętrznym i koszcie chipów oraz różnic w technikach programowania na poziomie języka asemblera.

Wniosek

Powodem niemożności posiadania uniwersalnego języka asemblera jest to, że aby poprawnie przekonwertować kod asemblera z jednego zestawu instrukcji na inny, należy od nowa zaprojektować kod - czego nie mogą jeszcze zrobić komputery.


Doskonała odpowiedź! Ludzie nie rozumieją wystarczająco dobrze, że obliczenia, które należy zaprogramować, są wszędzie wśród nas. To nie tylko aplikacje działające na naszych ekranach. Ile miliardów wiórów produkuje się każdego roku?
phs

4

Dodając do cudownej odpowiedzi DW: jeśli chciałbyś mieć jednego asemblera, musiałby on utrzymywać wszystkie architektury, idealny między nimi tłumacz i w pełni zrozumieć, co robisz.
Niektóre wysoce zoptymalizowane kody na jedną architekturę musiałyby zostać dezoptymalizowane, zrozumiane na bardziej abstrakcyjnym poziomie i zoptymalizowane pod kątem innej.
Ale gdyby to było możliwe, mielibyśmy doskonały kompilator C, a pisanie w czystym asemblerze nie byłoby w ogóle korzystne.
Głównym punktem używania asemblera jest wydajność, której nie można wycisnąć z najnowszych kompilatorów.
Napisanie takiego programu byłoby jeszcze trudniejsze niż istniejące kompilatory, a utrzymanie wszystkich tworzonych architektur jeszcze bardziej utrudniłoby.
A dla programu „tylko jeden” oznaczałoby to również pełną zgodność wsteczną.


W zdecydowanej większości przypadków gcc wykonuje lepszą optymalizację niż programista. Głównym celem korzystania z asemblera jest robienie rzeczy, których nie można zrobić w C, takich jak dostęp do rejestrów. Jeśli spojrzysz na drzewo źródłowe Linuksa, to właściwie do czego używają asemblera.
slebetman

@slebetman - gcc pozwala umieścić zmienną w rejestrze bez uciekania się do asemblera.
Jirka Hanika,

@JirkaHanika: czy mówisz o rejestrach CPU lub rejestrach sprzętu specjalnego przeznaczenia, które są adresowane specjalnymi instrukcjami? Podejrzewam, że slebetman oznacza to drugie.
PJTraill

„Wszystkie kody” - „GCC robi lepiej” = „używasz asemblera”. Tak, masz dostęp do rejestrów bez wstawek asemblera.
Zło

@PJTraill - komentarz Slebetmana jest ogólnie doskonały i być może powinien zostać włączony do odpowiedzi. Ale oba jego przykłady (dostęp do rejestru i drzewo źródłowe Linuksa) prawdopodobnie podsycają powszechne nieporozumienia, a nie że byłyby to doskonałe przykłady tego, czego nie można zrobić w C z rozszerzeniami gcc; należy je wymienić lub pominąć. (Jeśli dziś jest instrukcja HW, aby coś zrobić, za rok otrzymasz odpowiednie rozszerzenie gcc. Nie zawsze, ale bardzo często. Przykłady się starzeją.)
Jirka Hanika

3

Microsoft wynalazł MSIL jako pośredni język asemblera. Programy będą kompilowane z C # lub VB.Net do MSIL. W czasie wykonywania plik MSIL został skompilowany do kodu maszynowego komputera, na którym był uruchomiony przy użyciu kompilatora JIT . Plik zawierający MSIL był plikiem .EXE z kilkoma instrukcjami na początku w X86, aby uruchomić program. W procesorze ARM należy wpisać słowo mono przed nazwą programu, aby go uruchomić.


Jaka jest różnica między „językiem asemblera pośredniego” a „maszyną wirtualną”?
Bob Jarvis - Przywróć Monikę

@BobJarvis: Jeden to kod, a drugi to tłumacz. Powinieneś był zapytać, jaka jest różnica między montażem pośrednim a
kodem

To nie wydaje się odpowiadać na pytanie. Tak długo, jak każda maszyna kompiluje / kompiluje MSIL inaczej, nie ma w tym nic uniwersalnego, a celem takiej kompilacji jest przenoszenie ogólnej funkcjonalności, a nie wykorzystywanie określonego zestawu instrukcji, który, jak zauważa DW, jest (lub a) powód korzystania z asemblera.
PJTraill

3

Jak wspomniano, LLVM jest jak dotąd najbliższą rzeczą. Dużą barierą dla naprawdę uniwersalnego języka będą fundamentalne różnice związane z domniemanymi kompromisami: współbieżność, wykorzystanie pamięci, przepustowość, opóźnienie i zużycie energii. Jeśli piszesz wyraźnie w stylu SIMD, możesz używać zbyt dużej ilości pamięci. Jeśli piszesz jawnie w stylu SISD, uzyskasz suboptymalną równoległość. Jeśli zoptymalizujesz przepustowość, zranisz opóźnienie. Jeśli zmaksymalizujesz przepustowość jednowątkową (tj. Prędkość zegara), zepsujesz żywotność baterii.

Przynajmniej kod musiałby być opatrzony adnotacjami z kompromisami. Najważniejsze może być to, że język ma dobre właściwości algebraiczne / typu, które dają kompilatorowi dużo miejsca na optymalizację i wykrywanie logicznej niespójności.

Następnie pojawia się kwestia nieokreślonego zachowania. Duża część języków C i asemblera pochodzi z nieokreślonego zachowania. Jeśli przyznajesz się do nieokreślonego zachowania, które faktycznie się dzieje, ostatecznie traktujesz je jako przypadki szczególne (np. Włamania do architektury i kontekstu).


0

Być może to, czego szukasz, to notacja Universal Turning Machine, w której wszyscy zgadzają się co do symboli poleceń. ( https://en.wikipedia.org/wiki/Universal_Turing_machine )

„Asembler”, który tłumaczy język dopuszczalny pod kątem Turning na kod maszynowy konkretnego dostawcy i może być budowany dla dowolnej z tych rzeczy, które nazywamy komputerami.

W sztuce programowania komputerowego znajduje się przykład tego, jak może to wyglądać.

Zastanówmy się jednak nad pytaniem „dlaczego nie jest to dostępny na rynku uniwersalny język, którego można używać na wszystkich komputerach”. Sugeruję, że najbardziej dominującymi wpływami są (1) wygoda, nie wszystkie języki asemblera są najbardziej wygodne w użyciu; (2) ekonomia, zapewnienie, niekompatybilność maszyn różnych marek i sprzedawców to strategia biznesowa, a także wynik ograniczonych zasobów (czasu / pieniędzy) na projektowanie maszyn.


Pytanie dotyczy języka asemblera, którego można użyć do programowania dowolnego komputera, a nie języka asemblera uniwersalnego w znaczeniu „uniwersalnej maszyny Turinga”.
David Richerby,

1
Church-Turing mówi nam, że UTC może zrobić to, co potrafi każdy programowalny komputer. Oprócz skończonych problemów z fizycznym przechowywaniem. Język asemblera dla UTC jest całkiem wykonalny. Ale, jak powiedziałem, praktyczność kulturowa i ekonomiczna może ograniczać faktyczne wdrażanie i adopcję na rynku.
Chris

Brakuje największego problemu, jakim jest wydajność ! Po co używać języka 1000 razy wolniej tylko dla jakiegoś wzniosłego celu, jakim jest niezależność od sprzętu? Maszyna Turinga to okropny model do praktycznego korzystania z komputera.
Artelius

1
Czy komentatorzy chcieliby zaoferować jakąkolwiek informatykę na poparcie swoich roszczeń? To przecież forum informatyczne.
Chris

1
Nie jestem ekspertem od CS. Uważam jednak, że architektura von Neumann jest genialnym dziełem inżynierii, która zapewnia równowagę między programowalnością a wydajnością, podczas gdy celem maszyny Turinga jest pokazanie, że nawet najbardziej podstawowa maszyna może obliczyć wszystko, co mogłaby zrobić bardziej złożona maszyna. Oczywiście, możesz ciągle dodawać coraz więcej funkcji do maszyny Turinga (więcej taśm, arytmetyka), ale wtedy pojawia się ten sam problem, który miałeś na początku, a mianowicie ludzie, którzy nie zgadzają się na zestaw instrukcji. Ponadto brak dostępu losowego powoduje duże koszty ogólne w wielu algorytmach.
Artelius

0

założenie: kompilacja i optymalizacja języka wysokiego poziomu L1 do języka niższego poziomu L0 jest łatwiejsza niż kompilacja i optymalizacja języka wysokiego poziomu L2 (wyższego niż L1) do L0; łatwiejsze w tym sensie, że podobno można wygenerować bardziej zoptymalizowany kod podczas kompilacji L1 do L0 niż L2 do L0.

Myślę, że założenie jest prawdopodobnie poprawne, dlatego prawdopodobnie większość kompilatorów używa niskiego poziomu języka pośredniego (IR / LLVM).

jeśli jest to prawdą, użyj dowolnego L0 języka niskiego poziomu i napisz kompilatory, aby przetłumaczyć L0 na inne języki niskiego poziomu. Na przykład użyj zestawu instrukcji MIPS i skompiluj go do x86, arm, power, ...

-Taoufik


Więc nie wiesz, czy twoja odpowiedź jest prawdziwa? I nie może tego obsłużyć?
Zły
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.