Czy deklaratory typu danych, takie jak „int” i „char” są przechowywane w pamięci RAM podczas wykonywania programu w języku C?


74

Gdy program C jest uruchomiony, dane są przechowywane na stercie lub stosie. Wartości są przechowywane w adresach RAM. Ale co ze wskaźnikami typu (np. intLub char)? Czy są również przechowywane?

Rozważ następujący kod:

char a = 'A';
int x = 4;

Przeczytałem, że A i 4 są tutaj przechowywane w adresach RAM. Ale co ai x? Co najbardziej mylące, skąd egzekucja wie, że ajest char i xint? Mam na myśli, intczy charwspomniano gdzieś w pamięci RAM?

Powiedzmy, że wartość jest przechowywana gdzieś w pamięci RAM jako 10011001; jeśli jestem programem wykonującym kod, to skąd mam wiedzieć, czy ten 10011001 jest a, charczy int?

To, czego nie rozumiem, to skąd komputer wie, kiedy odczytuje wartość zmiennej z adresu takiego jak 10001, bez względu na to, czy jest to intlub char. Wyobraź sobie, że klikam program o nazwie anyprog.exe. Kod natychmiast zaczyna działać. Czy ten plik wykonywalny zawiera informacje, czy przechowywane zmienne są typu, intczy char?


24
Ta informacja jest całkowicie tracona w czasie wykonywania. Ty (i Twój kompilator) musisz wcześniej upewnić się, że pamięć zostanie poprawnie zinterpretowana. Czy to odpowiedź, której szukałeś?
5gon12eder,

4
Nie ma Ponieważ zakłada, że ​​wiesz, co robisz, bierze wszystko, co znajdzie pod podanym adresem pamięci, i zapisuje je na standardowe wyjście. Jeśli cokolwiek zostało napisane, odpowiada czytelnemu znakowi, ostatecznie pojawi się na czyjejś konsoli jako czytelny znak. Jeśli tak się nie zgadza, pojawi się jako bełkot lub losowo czytelna postać.
Robert Harvey,

22
@ user16307 Krótka odpowiedź jest taka, że ​​w językach o typie statycznym, za każdym razem, gdy wypisujesz znak, kompilator wygeneruje inny kod niż wydrukowałby int. W czasie wykonywania nie ma już żadnej wiedzy, która xjest char, ale uruchamiany jest kod drukujący char, ponieważ właśnie to wybrał kompilator.
Ixrec,

13
@ user16307 Zawsze jest przechowywany jako binarna reprezentacja liczby 65. To, czy zostanie wydrukowane jako 65, czy jako A, zależy od kodu wygenerowanego przez kompilator w celu wydrukowania. Obok 65 nie ma metadanych, które mówiłyby, że to w rzeczywistości znak lub int (przynajmniej nie w statycznie typowanych językach, takich jak C).
Ixrec,

2
Aby w pełni zrozumieć koncepcje, o które pytasz, i wdrożyć je samodzielnie, możesz wziąć udział w kursie kompilatora, np. Kurs Coursera
mucaho

Odpowiedzi:


122

Aby odpowiedzieć na pytanie, które opublikowałeś w kilku komentarzach (które moim zdaniem należy edytować w swoim poście):

To, czego nie rozumiem, to skąd komputer wie, kiedy odczytuje wartość zmiennej i adres, na przykład 10001, jeśli jest wartością int lub char. Wyobraź sobie, że klikam program o nazwie anyprog.exe. Kod natychmiast zaczyna działać. Czy ten plik exe zawiera informacje o tym, czy zmienne są przechowywane jako w lub char?

Dodajmy do tego trochę kodu. Powiedzmy, że piszesz:

int x = 4;

Załóżmy, że jest przechowywany w pamięci RAM:

0x00010004: 0x00000004

Pierwsza część to adres, druga część to wartość. Kiedy twój program (który wykonuje się jako kod maszynowy) działa, widzi 0x00010004tylko wartość 0x000000004. Nie „zna” tego typu danych i nie wie, w jaki sposób „powinien” zostać użyty.

Jak więc twój program wymyślił właściwą rzecz? Rozważ ten kod:

int x = 4;
x = x + 5;

Mamy tutaj przeczytanie i napisanie. Gdy program odczytuje xz pamięci, znajduje się 0x00000004tam. A twój program wie, jak go dodać 0x00000005. Powodem, dla którego Twój program „wie”, że jest to poprawna operacja, jest to, że kompilator zapewnia poprawność operacji dzięki bezpieczeństwu typu. Twój kompilator już zweryfikował, że możesz dodać 4i 5razem. Kiedy więc uruchamia się twój kod binarny (exe), nie trzeba go weryfikować. Po prostu wykonuje każdy krok na ślepo, zakładając, że wszystko jest w porządku (złe rzeczy zdarzają się, gdy w rzeczywistości nie są w porządku).

Inny sposób myślenia o tym jest taki. Dam ci te informacje:

0x00000004: 0x12345678

Taki sam format jak poprzednio - adres po lewej, wartość po prawej. Jakiego typu jest wartość? W tym momencie znasz tyle samo informacji o tej wartości, co twój komputer, gdy wykonuje kod. Gdybym kazał ci dodać 12743 do tej wartości, możesz to zrobić. Nie masz pojęcia, jakie będą konsekwencje tej operacji dla całego systemu, ale dodanie dwóch liczb to coś, w czym jesteś naprawdę dobry, więc możesz to zrobić. Czy to sprawia, że ​​wartość jest an int? Niekoniecznie - widać tylko 32-bitowe wartości i operator dodawania.

Być może pewnym zamieszaniem jest odzyskanie danych. Jeśli mamy:

char A = 'a';

Skąd komputer wie, że wyświetla się aw konsoli? Jest na to wiele kroków. Pierwszym z nich jest przejście do Alokalizacji w pamięci i odczytanie jej:

0x00000004: 0x00000061

Wartość szesnastkowa aw ASCII wynosi 0x61, więc powyższe może być czymś, co zobaczysz w pamięci. Teraz nasz kod maszynowy zna wartość całkowitą. Skąd wie, że zmienia wartość całkowitą w znak, aby ją wyświetlić? Mówiąc najprościej, kompilator wykonał wszystkie niezbędne kroki, aby dokonać tego przejścia. Ale sam komputer (lub program / exe) nie ma pojęcia, jaki jest typ tych danych. Ta 32-bitowa wartość może być dowolna - int, charpołowa double, wskaźnik, część tablicy, część string, część instrukcji itp.


Oto krótka interakcja Twojego programu (exe) z komputerem / systemem operacyjnym.

Program: chcę zacząć. Potrzebuję 20 MB pamięci.

System operacyjny: znajduje 20 wolnych MB pamięci, które nie są używane, i przekazuje je

(Ważna uwaga jest taka, że ​​może to zwrócić dowolne 20 wolnych MB pamięci, nie muszą nawet być ciągłe. W tym momencie program może teraz działać w pamięci, którą posiada, bez rozmowy z systemem operacyjnym)

Program: Zakładam, że pierwszym miejscem w pamięci jest 32-bitowa zmienna całkowita x.

(Kompilator upewnia się, że dostęp do innych zmiennych nigdy nie dotknie tego miejsca w pamięci. W systemie nic nie mówi, że pierwszy bajt jest zmienny xlub ta zmienna xjest liczbą całkowitą. Analogia: masz torbę. Mówisz ludziom, że umieścisz w tej torbie tylko żółte kulki. Gdy ktoś później wyciągnie coś z torby, szokujące byłoby wyciągnięcie czegoś niebieskiego lub sześcianu - coś poszło strasznie nie tak. To samo dotyczy komputerów: twój program przyjmuje teraz, że pierwszym miejscem w pamięci jest zmienna x i że jest liczbą całkowitą. Jeśli w tym bajcie pamięci zostanie kiedykolwiek napisane coś innego lub zakłada się, że jest to coś innego - wydarzyło się coś strasznego. Kompilator zapewnia, że ​​takie rzeczy nie się zdarzyło)

Program: Teraz napiszę 2do pierwszych czterech bajtów, w których, jak zakładam, xjest.

Program: Chcę dodać 5 do x.

  • Odczytuje wartość X do rejestru tymczasowego

  • Dodaje 5 do rejestru tymczasowego

  • Przechowuje wartość rejestru tymczasowego z powrotem w pierwszym bajcie, który nadal jest przyjmowany x.

Program: założę, że następnym dostępnym bajtem jest zmienna char y.

Program: Napiszę ado zmiennej y.

  • Biblioteka służy do znalezienia wartości bajtu dla a

  • Bajt jest zapisywany na adres, który zakłada program y.

Program: Chcę wyświetlić zawartość y

  • Odczytuje wartość w drugim miejscu pamięci

  • Używa biblioteki do konwersji z bajtu na znak

  • Używa bibliotek graficznych do zmiany ekranu konsoli (ustawianie pikseli z czarnego na biały, przewijanie jednej linii itp.)

(I zaczyna się stąd)

To, co prawdopodobnie Cię rozłącza, to - co dzieje się, gdy nie ma już pierwszego miejsca w pamięci x? czy drugi już nie jest y? Co się dzieje, gdy ktoś czyta xjako wskaźnik charlub ywskaźnik? Krótko mówiąc, zdarzają się złe rzeczy. Niektóre z tych rzeczy mają dobrze zdefiniowane zachowanie, a niektóre mają niezdefiniowane zachowanie. Nieokreślone zachowanie jest dokładnie tym - wszystko może się zdarzyć, od niczego, po awarię programu lub systemu operacyjnego. Nawet dobrze zdefiniowane zachowanie może być złośliwe. Jeśli mogę zmienić xwskaźnik na mój program i sprawić, by Twój program używał go jako wskaźnika, to mogę sprawić, że Twój program zacznie uruchamiać mój program - właśnie to robią hakerzy. Kompilator pomaga upewnić się, że nie używamy go int xjakostringi rzeczy tego rodzaju. Sam kod maszynowy nie jest świadomy typów i robi tylko to, co nakazują instrukcje. Istnieje również duża ilość informacji odkrytych w czasie wykonywania: z których bajtów pamięci może korzystać program? Czy xzaczyna się od pierwszego bajtu, czy od 12?

Ale możesz sobie wyobrazić, jak okropnie byłoby pisać takie programy (i możesz to zrobić w języku asemblera). Zaczynasz od „zadeklarowania” zmiennych - mówisz sobie, że bajt 1 to xbajt 2 y, a kiedy piszesz każdy wiersz kodu, ładując i przechowując rejestry, musisz (jako człowiek) pamiętać, który jest, xa który jeden jest y, ponieważ system nie ma pojęcia. A ty (jako człowiek) musisz pamiętać, jakie typy xi jakie ysą, ponieważ znowu - system nie ma pojęcia.


Niesamowite wyjaśnienie. Tylko ta część, którą napisałeś: „Skąd wiadomo, że zmienia liczbę całkowitą w znak, aby ją wyświetlić? Po prostu, kompilator wykonał wszystkie niezbędne kroki, aby dokonać tego przejścia”. nadal jest dla mnie mglisty. Powiedzmy, że procesor pobrał 0x00000061 z rejestru RAM. Od tego momentu mówisz, że istnieją inne instrukcje (w pliku exe), które powodują przejście do tego, co widzimy na ekranie?
użytkownik16307,

2
@ user16307 tak, są dodatkowe instrukcje. Każdy wiersz kodu, który piszesz, może potencjalnie zostać przekształcony w wiele instrukcji. Istnieją instrukcje, aby dowiedzieć się, jakiego znaku użyć, są instrukcje, dla których pikseli należy zmodyfikować i jaki kolor zmieniają itp. Istnieje również kod, którego tak naprawdę nie widać. Na przykład użycie std :: cout oznacza, że ​​korzystasz z biblioteki. Kod do napisania w konsoli może być tylko jedną linią, ale wywoływaną (-e) funkcją (-ami) będzie więcej linii, a każda linia może zmienić się w wiele instrukcji maszynowych.
Shaz

8
@ user16307 Otherwise how can console or text file outputs a character instead of int Ponieważ istnieje inna sekwencja instrukcji wyprowadzania zawartości lokalizacji w pamięci jako liczba całkowita lub jako znaki alfanumeryczne. Kompilator wie o typach zmiennych, wybiera odpowiednią sekwencję instrukcji w czasie kompilacji i zapisuje ją w EXE.
Charles E. Grant,

2
Znalazłbym inną frazę dla „samego kodu bajtowego”, ponieważ kod bajtowy (lub kod bajtowy) zwykle odnosi się do języka pośredniego (takiego jak Java Bytecode lub MSIL), który może faktycznie przechowywać te dane, aby środowisko wykonawcze mogło je wykorzystać. Ponadto nie jest całkowicie jasne, do jakiego „kodu bajtowego” ma się odnosić w tym kontekście. W przeciwnym razie fajna odpowiedź.
jpmc26,

6
@ user16307 Staraj się nie martwić o C ++ i C #. To, co mówią ci ludzie, znacznie przewyższa twoje obecne rozumienie działania komputerów i kompilatorów. Dla celów tego, co próbujesz zrozumieć, sprzęt NIE wie nic o typach, char, int lub cokolwiek innego. Kiedy powiedziałeś kompilatorowi, że zmienna jest liczbą całkowitą, wygenerował kod wykonywalny do obsługi lokalizacji pamięci JAK JEŚLI była to liczba całkowita. Sama lokalizacja pamięci nie zawiera informacji o typach; po prostu twój program postanowił potraktować to jako int. Zapomnij o wszystkim, co słyszałeś na temat informacji o typie środowiska wykonawczego.
Andres F.,

43

Wydaje mi się, że twoim głównym pytaniem jest: „Jeśli typ zostanie usunięty w czasie kompilacji i nie zostanie zachowany w czasie wykonywania, to skąd komputer wie, czy wykonać kod, który interpretuje go jako, intczy wykonać kod, który interpretuje go jako char? „

Odpowiedź brzmi… komputer nie. Jednak kompilator nie wie i to będzie po prostu umieścić poprawny kod w pliku binarnego w pierwszej kolejności. Gdyby zmienna została wpisana jako char, to kompilator nie umieściłby kodu do traktowania jej jako intprogramu, a kod potraktowałby ją jako char.

Tam powody, aby zachować typ w czasie wykonywania:

  • Pisanie dynamiczne: podczas pisania dynamicznego sprawdzanie typów odbywa się w czasie wykonywania, więc oczywiście typ musi być znany w czasie wykonywania. Ale C nie jest dynamicznie wpisywane, więc typy można bezpiecznie usunąć. (Należy jednak pamiętać, że jest to zupełnie inny scenariusz. Typy dynamiczne i typy statyczne to tak naprawdę nie to samo, a w języku pisania mieszanego można nadal wymazywać typy statyczne i zachowywać tylko typy dynamiczne).
  • Dynamiczny polimorfizm: jeśli wykonujesz inny kod w zależności od typu środowiska wykonawczego, musisz zachować ten typ środowiska. C nie ma dynamicznego polimorfizmu (tak naprawdę nie ma żadnego polimorfizmu, z wyjątkiem niektórych specjalnych przypadków zakodowanych na stałe, np. +Operatora), więc z tego powodu nie potrzebuje typu środowiska wykonawczego. Jednak znowu typ środowiska wykonawczego jest czymś innym niż typ statyczny, np. W Javie można teoretycznie usunąć typy statyczne i nadal zachować typ środowiska wykonawczego dla polimorfizmu. Zauważ też, że jeśli zdecentralizujesz i specjalizujesz kod wyszukiwania typu i umieścisz go w obiekcie (lub klasie), to niekoniecznie będziesz potrzebował typu środowiska wykonawczego, np. Vtables C ++.
  • Odbicie środowiska wykonawczego: jeśli pozwalasz programowi zastanawiać się nad typami w środowisku wykonawczym, oczywiście musisz zachować typy w środowisku wykonawczym. Możesz to łatwo zobaczyć w Javie, która utrzymuje typy pierwszego rzędu w czasie wykonywania, ale usuwa argumenty typu do typów ogólnych w czasie kompilacji, więc możesz zastanowić się tylko nad konstruktorem typów („typ surowy”), ale nie argumentem typu. Znów C nie ma odzwierciedlenia środowiska wykonawczego, więc nie musi utrzymywać tego typu w środowisku wykonawczym.

Jedynym powodem, aby utrzymać typ w środowisku wykonawczym w C, jest debugowanie, jednak debugowanie zwykle odbywa się przy dostępnym źródle, a następnie można po prostu wyszukać typ w pliku źródłowym.

Usuwanie typu jest całkiem normalne. Nie wpływa to na bezpieczeństwo typu: typy są sprawdzane w czasie kompilacji, gdy kompilator upewni się, że program jest bezpieczny dla typu, typy nie są już potrzebne (z tego powodu). Nie wpływa na statyczny polimorfizm (inaczej przeciążenie): po zakończeniu rozwiązywania problemu z przeciążeniem, a kompilator wybrał odpowiednie przeciążenie, nie potrzebuje już typów. Typy mogą również kierować optymalizacją, ale ponownie, gdy optymalizator wybierze optymalizacje na podstawie typów, nie będzie ich już potrzebował.

Zachowywanie typów w środowisku wykonawczym jest wymagane tylko wtedy, gdy chcesz coś zrobić z typami w środowisku wykonawczym.

Haskell jest jednym z najbardziej rygorystycznych, najbardziej rygorystycznych, bezpiecznych dla języka statycznych typów, a kompilatory Haskell zwykle usuwają wszystkie typy. (Uważam, że wyjątkiem jest przekazywanie słowników metod dla klas typów).


3
Nie! Dlaczego? Do czego potrzebne byłyby te informacje? Kompilator wysyła kod do odczytu a chardo skompilowanego pliku binarnego. To nie ma wyjścia dla kodu int, to nie ma wyjścia dla kodu byte, to nie wyjście kod do wskaźnika, to po prostu wyświetla tylko kod dla char. Na podstawie typu nie są podejmowane żadne decyzje dotyczące środowiska wykonawczego. Nie potrzebujesz tego typu. Jest to całkowicie i całkowicie nieistotne. Wszystkie istotne decyzje zostały już podjęte w czasie kompilacji.
Jörg W Mittag,

2
Nie ma Kompilator po prostu umieszcza kod do wypisania znaku w pliku binarnym. Kropka. Kompilator wie, że pod tym adresem pamięci znajduje się znak char, dlatego umieszcza kod do wypisania znaku w pliku binarnym. Jeśli wartość pod tym adresem pamięci z jakiegoś dziwnego powodu zdarza się, że nie jest char, to cóż, piekło się rozprasza. W ten sposób działa cała klasa exploitów bezpieczeństwa.
Jörg W Mittag,

2
Pomyśl o tym: jeśli procesor w jakiś sposób wiedział o typach danych programów, to wszyscy na świecie musieliby kupić nowy procesor za każdym razem, gdy ktoś wymyśli nowy typ. public class JoergsAwesomeNewType {};Widzieć? Właśnie wymyśliłem nowy typ! Musisz kupić nowy procesor!
Jörg W Mittag,

9
Nie. Nie ma. Kompilator wie, jaki kod musi umieścić w pliku binarnym. Nie ma sensu utrzymywać tych informacji w pobliżu. Jeśli drukujesz int, kompilator umieści kod do wydrukowania int. Jeśli drukujesz znak, kompilator umieści kod do drukowania znaku. Kropka. Ale to tylko trochę wzorzec. Kod do drukowania znaku interpretuje wzór bitowy w określony sposób, kod do drukowania int interpretuje bit w inny sposób, ale nie ma sposobu na odróżnienie wzoru bitowego, który jest int od wzoru bitowego, który jest char, to ciąg bitów.
Jörg W Mittag,

2
@ user16307: „Czy plik exe nie zawiera informacji o tym, jaki adres to jaki typ danych?” Może. Jeśli kompilujesz z danymi debugowania, dane debugowania będą zawierać informacje o nazwach zmiennych, adresach i typach. Czasami dane debugowania są przechowywane w pliku .exe (jako strumień binarny). Ale nie jest częścią kodu wykonywalnego i nie jest używana przez samą aplikację, tylko przez debugger.
Ben Voigt,

12

Komputer nie „wie”, jakie są adresy, ale znajomość tego, co jest zapisane w instrukcjach programu.

Kiedy piszesz program C, który zapisuje i odczytuje zmienną char, kompilator tworzy kod asemblera, który zapisuje ten fragment danych gdzieś jako char, a także gdzieś indziej kod, który odczytuje adres pamięci i interpretuje go jako char. Jedyne, co łączy te dwie operacje razem, to lokalizacja tego adresu pamięci.

Kiedy przychodzi czas na czytanie, instrukcje nie mówią „zobacz, jaki jest typ danych”, po prostu mówi coś w rodzaju „załaduj tę pamięć jako liczbę zmiennoprzecinkową”. Jeśli adres do odczytu zostanie zmieniony lub coś zastąpi tę pamięć czymś innym niż zmiennoprzecinkowe, procesor po prostu z przyjemnością załaduje tę pamięć jako zmiennoprzecinkowe, w wyniku czego mogą się zdarzyć różne dziwne rzeczy.

Zły czas na analogię: wyobraź sobie skomplikowany magazyn wysyłkowy, w którym magazyn to pamięć, a ludzie wybierają rzeczy to procesor. Jedna część „programu” magazynu umieszcza różne przedmioty na półce. Kolejny program idzie i zabiera przedmioty z magazynu i umieszcza je w pudełkach. Kiedy są ściągane, nie są sprawdzane, po prostu wchodzą do kosza. Cały magazyn funkcjonuje, wszystko synchronizuje się, a właściwe przedmioty zawsze znajdują się we właściwym miejscu we właściwym czasie, w przeciwnym razie wszystko ulega awarii, tak jak w prawdziwym programie.


jak byś wytłumaczył, jeśli CPU znajdzie 0x00000061 w rejestrze i pobierze go; i wyobraź sobie, że program konsoli powinien wypisać to jako znak inny niż int. Czy masz na myśli, że w tym pliku exe jest kilka kodów instrukcji, które wiedzą, że adres 0x00000061 jest znakiem i konwertuje się na znak przy użyciu tabeli ASCII?
user16307,

7
Pamiętaj, że „wszystko ulega awarii” to w rzeczywistości najlepszy scenariusz. „Dziwne rzeczy się zdarzają” to drugi najlepszy scenariusz, „subtelnie dziwne rzeczy się zdarzają” są jeszcze gorsze, a najgorsze to „rzeczy dzieją się za twoimi plecami, które ktoś celowo zmanipulował tak, jak chcą”, znany także jako exploit bezpieczeństwa.
Jörg W Mittag,

@ user16307: Kod w programie każe komputerowi pobrać ten adres, a następnie wyświetlić go zgodnie z używanym kodowaniem. Bez względu na to, czy dane w lokalizacji pamięci są znakami ASCII czy pełnymi śmieciami, komputer nie jest tym zainteresowany. Coś innego było odpowiedzialne za ustawienie tego adresu pamięci, aby zawierał oczekiwane wartości. Myślę, że skorzystanie z programowania asemblera może być korzystne.
whatsisname

1
@ JörgWMittag: rzeczywiście. Pomyślałem o podaniu przepełnienia bufora jako przykładu, ale zdecydowałem, że sprawi to, że sprawy będą bardziej mylące.
whatsisname

@ user16307: Rzeczą, która wyświetla dane na ekranie, jest program. W tradycyjnym unixenie jest to terminal (oprogramowanie, które emuluje terminal szeregowy DEC VT100 - urządzenie sprzętowe z monitorem i klawiaturą, które wyświetla wszystko, co wchodzi do modemu do monitora i wysyła wszystko, co wpisane na klawiaturze do modemu). W systemie DOS jest to system DOS (właściwie tryb tekstowy karty VGA, ale zignorujmy to), a w systemie Windows to Command.com. Twój program nie wie, że faktycznie drukuje ciągi, po prostu drukuje sekwencję bajtów (liczb).
slebetman

8

Tak nie jest. Po skompilowaniu C do kodu maszynowego maszyna widzi tylko kilka bitów. Sposób interpretacji tych bitów zależy od tego, jakie operacje są na nich wykonywane, w przeciwieństwie do niektórych dodatkowych metadanych.

Typy, które wpisujesz w kodzie źródłowym, są tylko dla kompilatora. Przyjmuje, jaki typ danych ma być, i najlepiej jak potrafi, stara się upewnić, że dane te są wykorzystywane tylko w sensowny sposób. Gdy kompilator wykona jak najlepszą robotę, sprawdzając logikę kodu źródłowego, konwertuje go na kod maszynowy i odrzuca dane typu, ponieważ kod maszynowy nie ma możliwości jego przedstawienia (przynajmniej na większości komputerów) .


To, czego nie rozumiem, to skąd komputer wie, kiedy odczytuje wartość zmiennej i adres, na przykład 10001, jeśli jest int lub char. Wyobraź sobie, że klikam program o nazwie anyprog.exe. Kod natychmiast zaczyna działać. Czy ten plik exe zawiera informacje o tym, czy zmienne są przechowywane jako w lub char? -
user16307,

@ user16307 Nie, nie ma żadnych dodatkowych informacji o tym, czy coś jest int lub char. Później dodam kilka przykładów, zakładając, że nikt inny mnie nie pokona.
8bittree

1
@ user16307: Plik exe zawiera te informacje pośrednio. Procesor wykonujący program nie dba o typy używane podczas pisania programu, ale wiele z niego można wywnioskować z instrukcji używanych do uzyskania dostępu do różnych lokalizacji pamięci.
Bart van Ingen Schenau

@ user16307 jest tak naprawdę trochę dodatkowych informacji. Pliki exe wiedzą, że liczba całkowita ma 4 bajty, więc kiedy piszesz „int a”, kompilator przechowuje 4 bajty dla zmiennej i może w ten sposób obliczyć adres ai innych zmiennych później.
Esben Skov Pedersen

1
@ user16307 nie ma praktycznej różnicy (oprócz wielkości typu) różnicy między int a = 65i char b = 'A'po skompilowaniu kodu.

6

Większość procesorów udostępnia różne instrukcje dotyczące pracy z danymi różnych typów, więc informacje o typie są zwykle „wstawiane” do generowanego kodu maszynowego. Nie ma potrzeby przechowywania dodatkowych metadanych typu.

Niektóre konkretne przykłady mogą pomóc. Poniższy kod maszynowy został wygenerowany przy użyciu gcc 4.1.2 na systemie x86_64 z systemem SuSE Linux Enterprise Server (SLES) 10.

Załóżmy następujący kod źródłowy:

int main( void )
{
  int x, y, z;

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

Oto treść wygenerowanego kodu asemblera odpowiadającego powyższemu źródłu (za pomocą gcc -S), z dodanymi przeze mnie komentarzami:

main:
.LFB2:
        pushq   %rbp               ;; save the current frame pointer value
.LCFI0:
        movq    %rsp, %rbp         ;; make the current stack pointer value the new frame pointer value
.LCFI1:                            
        movl    $1, -12(%rbp)      ;; x = 1
        movl    $2, -8(%rbp)       ;; y = 2
        movl    -8(%rbp), %eax     ;; copy the value of y to the eax register
        addl    -12(%rbp), %eax    ;; add the value of x to the eax register
        movl    %eax, -4(%rbp)     ;; copy the value in eax to z
        movl    $0, %eax           ;; eax gets the return value of the function
        leave                      ;; exit and restore the stack
        ret

Oto kilka dodatkowych rzeczy ret, ale nie ma to znaczenia w dyskusji.

%eaxto 32-bitowy rejestr danych ogólnego przeznaczenia. %rspto 64-bitowy rejestr zarezerwowany do zapisywania wskaźnika stosu , który zawiera adres ostatniej rzeczy wypchniętej na stos. %rbpjest 64-bitowym rejestrem zarezerwowanym do zapisywania wskaźnika ramki , który zawiera adres bieżącej ramki stosu . Ramka stosu jest tworzona na stosie po wprowadzeniu funkcji i rezerwuje miejsce na argumenty funkcji i zmienne lokalne. Dostęp do argumentów i zmiennych można uzyskać za pomocą przesunięć wskaźnika ramki. W tym przypadku pamięć dla zmiennej xwynosi 12 bajtów „poniżej” adresu zapisanego w %rbp.

W powyższym kodzie kopiujemy wartość całkowitą x(1, przechowywaną w -12(%rbp)) do rejestru %eaxza pomocą movlinstrukcji, która służy do kopiowania 32-bitowych słów z jednej lokalizacji do drugiej. Następnie wywołujemy addl, co dodaje wartość całkowitą y(przechowywaną w -8(%rbp)) do wartości już w %eax. Następnie zapisujemy wynik -4(%rbp), czyli z.

Teraz zmieńmy to, abyśmy mieli do czynienia z doublewartościami zamiast intwartościami:

int main( void )
{
  double x, y, z;

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

gcc -SPonowne uruchomienie daje nam:

main:
.LFB2:
        pushq   %rbp                              
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movabsq $4607182418800017408, %rax ;; copy literal 64-bit floating-point representation of 1.00 to rax
        movq    %rax, -24(%rbp)            ;; save rax to x
        movabsq $4611686018427387904, %rax ;; copy literal 64-bit floating-point representation of 2.00 to rax
        movq    %rax, -16(%rbp)            ;; save rax to y
        movsd   -24(%rbp), %xmm0           ;; copy value of x to xmm0 register
        addsd   -16(%rbp), %xmm0           ;; add value of y to xmm0 register
        movsd   %xmm0, -8(%rbp)            ;; save result to z
        movl    $0, %eax                   ;; eax gets return value of function
        leave                              ;; exit and restore the stack
        ret

Kilka różnic. Zamiast movli addlużywamy movsdi addsd(przypisujemy i dodajemy zmiennoprzecinkowe podwójnej precyzji). Zamiast przechowywać wartości pośrednie %eax, używamy %xmm0.

Mam na myśli to, co mówię, gdy typ jest „wstawiany” do kodu maszynowego. Kompilator po prostu generuje odpowiedni kod maszynowy do obsługi tego konkretnego typu.


4

Historycznie C uważał pamięć za złożoną z kilku grup ponumerowanych miejsc typuunsigned char(zwany także „bajtem”, chociaż nie zawsze musi to być 8 bitów). Każdy kod, który wykorzystywałby cokolwiek przechowywanego w pamięci, musiałby wiedzieć, w którym gnieździe lub gniazdach była przechowywana informacja i wiedzieć, co należy zrobić z zawartymi tam informacjami [np. ”Zinterpretować cztery bajty zaczynające się pod adresem 123: 456 jako 32-bitowe wartość zmiennoprzecinkowa ”lub„ zapisz 16 bitów ostatnio obliczonej wielkości w dwóch bajtach, zaczynając od adresu 345: 678]. Sama pamięć nie wiedziałaby ani nie obchodziło, co wartości przechowywane w gniazdach pamięci „oznaczają”. kod próbował zapisać pamięć przy użyciu jednego typu i odczytać ją jako inny, wzorce bitów zapisane przez zapis byłyby interpretowane zgodnie z regułami drugiego typu, bez względu na konsekwencje.

Na przykład, jeśli kod ma zostać zapisany 0x12345678w wersji 32-bitowej unsigned int, a następnie spróbuj odczytać dwie kolejne 16-bitowe unsigned intwartości z jego adresu i powyższej, to w zależności od tego, w której połowie unsigned intzapisano, gdzie kod może odczytać wartości 0x1234 i 0x5678 lub 0x5678 i 0x1234.

Standard C99 nie wymaga jednak, aby pamięć zachowywała się jak grupa numerowanych gniazd, które nie wiedzą nic o tym, co reprezentują ich wzory bitowe . Kompilator może zachowywać się tak, jakby gniazda pamięci były świadome typów danych, które są w nich przechowywane, i zezwala tylko na dane, które są zapisywane przy użyciu dowolnego innego rodzaju niż unsigned chardo odczytu przy użyciu dowolnego typu unsigned charlub tego samego typu, w jakim zostały zapisane z; kompilatory mogą dalej zachowywać się tak, jakby gniazda pamięci miały moc i skłonność do arbitralnego niszczenia zachowania każdego programu, który próbuje uzyskać dostęp do pamięci w sposób sprzeczny z tymi regułami.

Dany:

unsigned int a = 0x12345678;
unsigned short p = (unsigned short *)&a;
printf("0x%04X",*p);

niektóre implementacje mogą drukować 0x1234, a inne mogą drukować 0x5678, ale zgodnie ze standardem C99 implementacja może drukować „ZASADY FRINK!” lub zrobić cokolwiek innego, zgodnie z teorią, zgodnie z którą legalne byłoby, aby lokalizacje pamięci azawierały sprzęt, który rejestruje, jakiego typu użyto do ich zapisania, oraz aby taki sprzęt reagował w jakikolwiek sposób na nieprawidłową próbę odczytu, w tym powodując „ZASADY FRINK!” być wyjściem.

Zauważ, że nie ma znaczenia, czy taki sprzęt faktycznie istnieje - fakt, że taki sprzęt mógłby legalnie istnieć, pozwala kompilatorom generować kod, który zachowuje się tak, jakby działał w takim systemie. Jeśli kompilator może ustalić, że określona lokalizacja pamięci zostanie zapisana jako jeden typ, a odczytana jako inny, może udawać, że działa w systemie, którego sprzęt może dokonać takiego określenia, i może zareagować z dowolnym stopniem kaprysu, jaki autor kompilatora uzna za stosowny. .

Celem tej reguły było umożliwienie kompilatorom, które wiedziały, że grupa bajtów o wartości pewnego typu posiadała określoną wartość w pewnym momencie i że od tego czasu nie zapisano żadnej wartości tego samego typu, aby wnioskować o tej grupie bajtów nadal utrzymywałoby tę wartość. Na przykład procesor wczytał grupę bajtów do rejestru, a następnie chciał ponownie użyć tych samych informacji, gdy był jeszcze w rejestrze, kompilator mógł użyć zawartości rejestru bez konieczności ponownego odczytu wartości z pamięci. Przydatna optymalizacja. Przez około pierwsze dziesięć lat obowiązywania reguły naruszenie tego oznaczałoby na ogół, że jeśli zmienna zostanie zapisana innym typem niż ten, który jest używany do jej odczytu, zapis może wpływać na odczytaną wartość lub nie. Takie zachowanie może w niektórych przypadkach być katastrofalne, ale w innych przypadkach może być nieszkodliwe,

Jednak około 2009 roku autorzy niektórych kompilatorów, takich jak CLANG, ustalili, że skoro Standard pozwala kompilatorom robić wszystko, co im się podoba w przypadkach, gdy pamięć jest zapisywana przy użyciu jednego typu i odczytywana jako inna, kompilatory powinny wywnioskować, że programy nigdy nie otrzymają danych wejściowych, które mogłyby spowodować coś takiego. Ponieważ Standard mówi, że kompilator może robić wszystko, co chce, po otrzymaniu takich nieprawidłowych danych wejściowych, kod, który miałby wpływ tylko w przypadkach, w których Standard nie nakłada żadnych wymagań, może (a zdaniem niektórych autorów kompilatora powinien zostać pominięty) jako nieistotne. Zmienia to zachowanie naruszeń aliasingu z tego, że przypomina pamięć, która przy danym żądaniu odczytu może dowolnie zwrócić ostatnią zapisaną wartość przy użyciu tego samego typu jak żądanie odczytu lub dowolną późniejszą wartość zapisaną przy użyciu innego typu,


1
Wzmianka o niezdefiniowanym zachowaniu podczas przycinania tekstu komuś, kto nie rozumie, jak nie ma RTTI, wydaje się sprzeczna z intuicją
Cole Johnson

@ColeJohnson: Szkoda, że ​​nie ma formalnej nazwy ani standardu dla dialektu języka C obsługiwanego przez 99% kompilatorów sprzed 2009 roku, ponieważ zarówno z perspektywy nauczania, jak i praktycznego, należy je uważać za zasadniczo różne języki. Ponieważ ta sama nazwa jest nadawana zarówno dialektowi, który ewoluował w ciągu 35 lat wielu przewidywalnych i możliwych do zoptymalizowania zachowań, dialekt, który wyrzuca takie zachowania w celu rzekomej optymalizacji, trudno jest uniknąć zamieszania, gdy mówi się o rzeczach, które działają w nich inaczej. .
supercat,

Historycznie C działał na maszynach Lisp, które nie pozwalały na tak luźną grę z typami. Jestem prawie pewien, że wiele „przewidywalnych i możliwych do zoptymalizowania zachowań” zaobserwowanych 30 lat temu po prostu nie działało nigdzie poza BSD Unix na VAX.
prosfilaes,

@prosfilaes: Być może „99% kompilatorów używanych w latach 1999–2009” byłoby bardziej dokładne? Nawet jeśli kompilatory miały opcje dla niektórych dość agresywnych optymalizacji liczb całkowitych, były one właśnie takimi - opcjami. Nie wiem, czy kiedykolwiek widziałem kompilator przed 1999 r., Który nie miał trybu, który nie gwarantowałby, że dane int x,y,z;wyrażenie x*y > znigdy nie zrobiłoby nic innego niż zwrócenie 1 lub 0, lub gdzie naruszenia aliasingu miałyby jakikolwiek skutek inne niż pozwolenie kompilatorowi na arbitralne zwrócenie starej lub nowej wartości.
supercat,

1
... gdzie unsigned charwartości, które są używane do budowy typu „pochodzą”. Jeśli program miałby rozkładać wskaźnik na unsigned char[], wyświetlać krótko jego zawartość szesnastkową na ekranie, a następnie usunąć wskaźnik unsigned char[], a następnie zaakceptować niektóre liczby szesnastkowe z klawiatury, skopiować je z powrotem do wskaźnika, a następnie oderwać ten wskaźnik , zachowanie byłoby dobrze zdefiniowane w przypadku, gdy wpisana liczba pasowała do wyświetlanej liczby.
supercat,

3

W C tak nie jest. Inne języki (np. Lisp, Python) mają typy dynamiczne, ale C ma typ statyczny. Oznacza to, że Twój program musi wiedzieć, jakiego typu dane mają poprawnie interpretować jako znak, liczba całkowita itp.

Zwykle kompilator dba o to, a jeśli zrobisz coś złego, pojawi się błąd kompilacji (lub ostrzeżenie).


To, czego nie rozumiem, to skąd komputer wie, kiedy odczytuje wartość zmiennej i adres, na przykład 10001, jeśli jest int lub char. Wyobraź sobie, że klikam program o nazwie anyprog.exe. Kod natychmiast zaczyna działać. Czy ten plik exe zawiera informacje o tym, czy zmienne są przechowywane jako w lub char? -
user16307,

1
@ user16307 Zasadniczo nie, wszystkie te informacje zostały całkowicie utracone. Kod maszynowy musi być zaprojektowany na tyle dobrze, aby dobrze wykonywać swoje zadania, nawet bez tych informacji. Cały komputer dba o to, że pod adresem znajduje się osiem bitów z rzędu 10001. Zadaniem użytkownika lub kompilatora jest , zależnie od przypadku, ręczne nadpisywanie takich rzeczy podczas pisania kodu maszyny lub zestawu.
Panzercrisis,

1
Pamiętaj, że dynamiczne pisanie nie jest jedynym powodem do zachowania typów. Java jest typowana statycznie, ale nadal musi zachowywać typy, ponieważ pozwala dynamicznie zastanawiać się nad typem. Dodatkowo ma polimorfizm środowiska wykonawczego, tj. Wysyłanie metod na podstawie typu środowiska wykonawczego, dla którego również potrzebuje tego typu. C ++ umieszcza kod metody wysyłania do samego obiektu (lub raczej klasy), więc nie potrzebuje on typu w pewnym sensie (chociaż oczywiście vtable jest w pewnym sensie częścią typu, więc naprawdę przynajmniej część typ jest zachowany), ale w Javie kod wysyłki metody jest scentralizowany.
Jörg W Mittag,

spójrz na moje pytanie, które napisałem „kiedy wykonuje się program C?” czyż nie są one pośrednio przechowywane w pliku exe wśród kodów instrukcji i ostatecznie zajmują miejsce w pamięci? Piszę to jeszcze raz: jeśli procesor znajdzie 0x00000061 w rejestrze i pobierze go; i wyobraź sobie, że program konsoli powinien wypisać to jako znak inny niż int. czy w tym pliku exe (kod maszynowy / binarny) są jakieś kody instrukcji, które znają, że adres 0x00000061 jest znakiem i konwertuje na znak przy użyciu tabeli ASCII? Jeśli tak, oznacza to, że identyfikatory char int są pośrednio w pliku binarnym ???
user16307,

Jeśli wartość wynosi 0x61 i jest zadeklarowana jako znak (tj. „A”), a wywoływana jest procedura w celu jej wyświetlenia, nastąpi [ostatecznie] wywołanie systemowe w celu wyświetlenia tego znaku. Jeśli zadeklarujesz go jako int i wywołasz procedurę wyświetlania, kompilator będzie wiedział, jak wygenerować kod do konwersji 0x61 (liczba dziesiętna 97) do sekwencji ASCII 0x39, 0x37 („9”, „7”). Konkluzja: generowany kod jest inny, ponieważ kompilator wie, że traktuje je inaczej.
Mike Harris,

3

Trzeba rozróżnić compiletimei runtimez jednej strony, a codei dataz drugiej strony.

Z punktu widzenia maszynowego to ma różnicy między tym, co nazywacie codealbo instructionsi co nazywasz data. Wszystko sprowadza się do liczb. Ale niektóre sekwencje - jak to nazwalibyśmy code- robią coś, co uważamy za przydatne, inne po prostu crashmaszynę.

Praca wykonywana przez CPU to prosta 4-etapowa pętla:

  • Pobierz „dane” z podanego adresu
  • Dekoduj instrukcję (tzn. „Interpretuj” liczbę jako instruction)
  • Przeczytaj skuteczny adres
  • Wykonuj i przechowuj wyniki

Nazywa się to cyklem instrukcji .

Przeczytałem, że A i 4 są tutaj przechowywane w adresach RAM. Ale co z a i x?

ai xsą zmiennymi, które są symbolami zastępczymi dla adresów, w których program może znaleźć „treść” zmiennych. Tak więc za każdym razem, gdy aużywana jest zmienna , efektywnie jest adres aużytej zawartości .

Co najbardziej mylące, skąd wykonanie wie, że a jest char, a x jest int?

Egzekucja nic nie wie. Z tego, co powiedziano we wstępie, CPU pobiera tylko dane i interpretuje je jako instrukcje.

Funkcja printf została zaprojektowana w taki sposób, aby „wiedziała”, jaki rodzaj danych w niej wkładasz, tj. Wynikowy kod zawiera odpowiednie instrukcje dotyczące postępowania ze specjalnym segmentem pamięci. Oczywiście możliwe jest generowanie nonsensownych danych wyjściowych: użycie adresu, w którym nie jest przechowywany żaden ciąg znaków wraz z „% s”, printf()spowoduje, że dane nonsensowne zostaną zatrzymane tylko przez losowe miejsce w pamięci, gdzie jest 0 ( \0).

To samo dotyczy punktu wejścia programu. Pod C64 możliwe było umieszczanie programów pod (prawie) każdym znanym adresem. Programy asemblacyjne rozpoczęto od instrukcji wywoływanej syspo adresie: sys 49152było to wspólne miejsce do umieszczania kodu asemblera. Ale nic nie powstrzymało Cię przed załadowaniem np. Danych graficznych 49152, co spowodowało awarię komputera po „uruchomieniu” od tego miejsca. W tym przypadku cykl instrukcji rozpoczął się od odczytu „danych graficznych” i próby interpretacji go jako „kodu” (co oczywiście nie miało sensu); efekty były zdumiewające;)

Powiedzmy, że wartość jest przechowywana gdzieś w pamięci RAM jako 10011001; jeśli jestem programem wykonującym kod, to skąd mam wiedzieć, czy ten 10011001 jest znakiem, czy intem?

Jak powiedziano: „Kontekst” - tj. Poprzednie i następne instrukcje - pomagają traktować dane tak, jak chcemy. Z perspektywy maszyny nie ma różnicy w żadnej lokalizacji pamięci. inti charjest tylko słownictwem, które ma sens compiletime; podczas runtime(na poziomie asemblera) nie ma charlub int.

Nie rozumiem tylko, skąd komputer wie, kiedy odczytuje wartość zmiennej z adresu takiego jak 10001, czy jest to int czy char.

Komputer nic nie wie . Programista robi. Skompilowany kod generuje kontekst , który jest niezbędny do generowania znaczących wyników dla ludzi.

Czy ten plik wykonywalny zawiera informacje o tym, czy przechowywane zmienne są typu int czy char

Tak i Nie . Informacje, niezależnie od tego, czy jest to intczy nie, charsą tracone. Ale z drugiej strony kontekst (instrukcje, jak radzić sobie z lokalizacjami pamięci, w których przechowywane są dane) jest zachowany; więc domyślnie tak, „informacja” jest domyślnie dostępna.


Ładne rozróżnienie między czasem kompilacji a czasem wykonywania.
Michael Blackburn,

2

Pozostawmy tę dyskusję tylko w języku C.

Program, o którym mowa, jest napisany w języku wysokiego poziomu, takim jak C. Komputer rozumie tylko język maszynowy. Języki wyższego poziomu dają programiście możliwość wyrażania logiki w sposób bardziej przyjazny dla człowieka, który jest następnie tłumaczony na kod maszynowy, który mikroprocesor może dekodować i wykonywać. Teraz omówmy wspomniany kod:

char a = 'A';
int x = 4;

Spróbujmy przeanalizować każdą część:

char / int są znane jako typy danych. Informują one kompilator o przydziale pamięci. W tym przypadku charbędzie to 1 bajt i int2 bajty. (Uwaga: ten rozmiar pamięci zależy ponownie od mikroprocesora).

a / x są znane jako identyfikatory. Teraz możesz powiedzieć „przyjazne dla użytkownika” nazwy nadane lokalizacjom pamięci w pamięci RAM.

= mówi kompilatorowi, aby zapisał „A” w miejscu pamięci ai 4 w miejscu pamięci x.

Tak więc identyfikatory typu danych int / char są używane tylko przez kompilator, a nie przez mikroprocesor podczas wykonywania programu. Dlatego nie są przechowywane w pamięci.


ok identyfikatory typu danych int / char nie są bezpośrednio przechowywane w pamięci jako zmienne, ale czy nie są one pośrednio przechowywane w pliku exe wśród kodów instrukcji i ostatecznie mają miejsce w pamięci? Piszę to jeszcze raz: jeśli procesor znajdzie 0x00000061 w rejestrze i pobierze go; i wyobraź sobie, że program konsoli powinien wypisać to jako znak inny niż int. czy w tym pliku exe (kod maszynowy / binarny) są jakieś kody instrukcji, które znają, że adres 0x00000061 jest znakiem i konwertuje na znak przy użyciu tabeli ASCII? Jeśli tak, oznacza to, że identyfikatory char int są pośrednio w pliku binarnym ???
user16307,

Nie, dla CPU wszystkie liczby. W twoim konkretnym przykładzie drukowanie na konsoli nie zależy od tego, czy zmienną jest char czy int. Zaktualizuję moją odpowiedź szczegółowym opisem konwersji programu wysokiego poziomu na język maszynowy do czasu wykonania programu.
prasad

2

Moja odpowiedź tutaj jest nieco uproszczona i będzie odnosić się tylko do C.

Nie, informacje o typie nie są zapisywane w programie.

intlub charnie są wskaźnikami typu do procesora; tylko do kompilatora.

Plik exe utworzony przez kompilator będzie miał instrukcje do manipulowania ints, jeśli zmienna została zadeklarowana jako int. Podobnie, jeśli zmienna została zadeklarowana jako a char, exe będzie zawierać instrukcje dotyczące manipulowania a char.

W C:

int main()
{
    int a = 65;
    char b = 'A';
    if(a == b)
    {
        printf("Well, what do you know. A char can equal an int.\n");
    }
    return 0;
}

Ten program wydrukuje swój komunikat, ponieważ chari intmają te same wartości w pamięci RAM.

Teraz, jeśli zastanawiasz się, jak printfudaje się wyprowadzić 65dla inti Adla char, oznacza to , że musisz określić w „ciągu formatu”, jak printfnależy traktować wartość .
(Na przykład %coznacza traktowanie wartości jako a chari %doznacza traktowanie wartości jako liczby całkowitej; jednak ta sama wartość w obu przypadkach.)


2
Miałem nadzieję, że ktoś użyje tego przykładu printf. @OP:int a = 65; printf("%c", a) wyświetli 'A'. Dlaczego? Ponieważ procesor nie dba o to. Do tego wszystko, co widzi, to kawałki. Twój program powiedział procesorowi, aby zapisał 65 (przypadkowo wartość 'A'w ASCII) w, aa następnie wypisał znak, co chętnie robi. Dlaczego? Ponieważ to nie obchodzi.
Cole Johnson

ale dlaczego niektórzy mówią tutaj w przypadku C #, to nie jest historia? czytam kilka innych komentarzy i mówią w C # i C ++ historia (informacje o typach danych) jest inna i nawet procesor nie wykonuje obliczeń. Jakieś pomysły na ten temat?
user16307,

@ user16307 Jeśli procesor nie wykonuje obliczeń, program nie działa. :) Co do C #, nie wiem, ale myślę, że moja odpowiedź dotyczy również tam. Jeśli chodzi o C ++, wiem, że moja odpowiedź ma tam zastosowanie.
BenjiWiebe,

0

Na najniższym poziomie w rzeczywistym fizycznym procesorze nie ma żadnych typów (ignorując jednostki zmiennoprzecinkowe). Po prostu wzory bitów. Komputer działa bardzo szybko, manipulując wzorami bitów.

To wszystko, co procesor kiedykolwiek robi, wszystko, co może zrobić. Nie ma czegoś takiego jak int lub char.

x = 4 + 5

Wykona się jako:

  1. Załaduj 00000100 do rejestru 1
  2. Załaduj 00000101 do rejestru 2
  3. Dodaj rejestr 1, aby zarejestrować 2 i zapisz w rejestrze 1

Instrukcja iadd uruchamia sprzęt, który zachowuje się tak, jakby rejestry 1 i 2 były liczbami całkowitymi. Jeśli tak naprawdę nie reprezentują liczb całkowitych, wszelkiego rodzaju rzeczy mogą później pójść nie tak. Najlepszy wynik to zwykle awaria.

Kompilator wybiera prawidłową instrukcję na podstawie typów podanych w źródle, ale w rzeczywistym kodzie maszynowym wykonanym przez CPU nie ma nigdzie żadnych typów.

edycja: Zauważ, że rzeczywisty kod maszynowy w rzeczywistości nie wspomina 4, 5, ani liczb całkowitych. to tylko dwa wzorce bitów i instrukcja, która bierze dwa wzorce bitowe, zakłada, że ​​są liczbami całkowitymi i dodaje je razem.


0

Krótka odpowiedź, typ jest zakodowany w instrukcjach procesora generowanych przez kompilator.

Chociaż informacje o typie lub rozmiarze informacji nie są bezpośrednio przechowywane, kompilator śledzi te informacje podczas uzyskiwania dostępu, modyfikowania i przechowywania wartości w tych zmiennych.

skąd wykonanie wie, że a jest char, a x jest int?

Nie robi tego, ale kiedy kompilator tworzy kod maszynowy, wie o tym. A inti a charmogą mieć różne rozmiary. W architekturze, w której char jest wielkością bajtu, a int jest 4 bajtami, wówczas zmienna xnie ma adresu 10001, ale także 10002, 10003 i 10004. Gdy kod musi załadować wartość xdo rejestru procesora, wykorzystuje instrukcję ładowania 4 bajtów. Podczas ładowania znaku używa instrukcji, aby załadować 1 bajt.

Jak wybrać jedną z dwóch instrukcji? Kompilator decyduje podczas kompilacji, nie jest to wykonywane w czasie wykonywania po sprawdzeniu wartości w pamięci.

Należy również pamiętać, że rejestry mogą mieć różne rozmiary. W procesorach Intel x86 EAX ma szerokość 32 bitów, połowa z nich to AX, czyli 16, a AX jest podzielony na AH i AL, oba 8 bitów.

Jeśli więc chcesz załadować liczbę całkowitą (na procesorach x86), użyj instrukcji MOV dla liczb całkowitych, aby załadować znak, użyj instrukcji MOV dla znaków. Oba są nazywane MOV, ale mają różne kody operacji. Skutecznie będąc dwiema różnymi instrukcjami. Typ zmiennej jest zakodowany w instrukcji użycia.

To samo dzieje się z innymi operacjami. Istnieje wiele instrukcji wykonywania dodawania, w zależności od wielkości operandów, a nawet jeśli są one podpisane lub niepodpisane. Zobacz https://en.wikipedia.org/wiki/ADD_(x86_instruction) które zawierają listę różnych możliwych dodatków.

Powiedzmy, że wartość jest przechowywana gdzieś w pamięci RAM jako 10011001; jeśli jestem programem wykonującym kod, to skąd będę wiedział, czy ten 10011001 jest znakiem, czy intem

Po pierwsze, char będzie wynosił 10011001, ale int będzie 00000000 00000000 00000000 10011001, ponieważ mają one różne rozmiary (na komputerze o takich samych rozmiarach, jak wspomniano powyżej). Ale rozważmy sprawę do signed charVS unsigned char.

To, co jest przechowywane w pamięci, można interpretować w dowolny sposób. Do obowiązków kompilatora C należy zapewnienie, że to, co jest przechowywane i odczytywane ze zmiennej, odbywa się w spójny sposób. Nie chodzi więc o to, że program wie, co jest przechowywane w miejscu pamięci, ale o to, że z góry zgadza się, że zawsze będzie tam czytać i pisać takie same rzeczy. (nie licząc rzeczy takich jak typy rzutowania).


ale dlaczego niektórzy mówią tutaj w przypadku C #, to nie jest historia? czytam kilka innych komentarzy i mówią w C # i C ++ historia (informacje o typach danych) jest inna i nawet procesor nie wykonuje obliczeń. Jakieś pomysły na ten temat?
user16307,

0

ale dlaczego niektórzy mówią tutaj w przypadku C #, to nie jest historia? czytam kilka innych komentarzy i mówią w C # i C ++ historia (informacje o typach danych) jest inna i nawet procesor nie wykonuje obliczeń. Jakieś pomysły na ten temat?

W językach z kontrolą typu, takich jak C #, sprawdzanie typu jest wykonywane przez kompilator. Kod benji napisał:

int main()
{
    int a = 65;
    char b = 'A';
    if(a == b)
    {
        printf("Well, what do you know. A char can equal an int.\n");
    }
    return 0;
}

Po prostu odmówiłby kompilacji. Podobnie, jeśli próbowałeś pomnożyć ciąg i liczbę całkowitą (chciałem powiedzieć add, ale operator „+” jest przeciążony konkatenacją łańcucha i może po prostu działać).

int a = 42;
string b = "Compilers are awesome.";
double[] c = a * b;

Kompilator po prostu odmówiłby wygenerowania kodu maszynowego z tego C #, bez względu na to, jak bardzo ciąg do niego pocałował.


-4

Pozostałe odpowiedzi są poprawne, ponieważ zasadniczo każde napotkane urządzenie konsumenckie nie przechowuje informacji o typie. W przeszłości istniało jednak kilka projektów sprzętowych (i obecnie, w kontekście badań), które wykorzystują oznakowaną architekturę - przechowują zarówno dane, jak i typ (i ewentualnie także inne informacje). Dotyczy to przede wszystkim maszyn Lisp .

Niejasno pamiętam, jak słyszałem o architekturze sprzętowej zaprojektowanej do programowania obiektowego, która miała coś podobnego, ale nie mogę jej teraz znaleźć.


3
Pytanie wyraźnie stwierdza, że ​​odnosi się do języka C (nie Lisp), a język C nie przechowuje zmiennych metadanych. Chociaż na pewno jest to możliwe w przypadku implementacji języka C, ponieważ standard tego nie zabrania, w praktyce tak się nigdy nie dzieje. Jeśli masz przykłady odnoszące się do kwestii, proszę podać konkretne cytaty i dostarczyć referencje , które odnoszą się do języka C .

Cóż, możesz napisać kompilator C dla maszyny Lisp, ale nikt nie używa maszyn Lisp w dzisiejszych czasach i ogólnie w wieku. Nawiasem mówiąc , architektura obiektowa to Rekursiv .
Nathan Ringo,

2
Myślę, że ta odpowiedź nie jest pomocna. Komplikuje sprawy znacznie wykraczające poza obecny poziom zrozumienia PO. Oczywiste jest, że OP nie rozumie podstawowego modelu wykonania CPU + RAM i tego, w jaki sposób kompilator tłumaczy symboliczne źródło wysokiego poziomu na wykonywalny plik binarny. Pamięć z tagami, RTTI, Lisp itp. To znacznie więcej niż to, o co pytający powinien wiedzieć w mojej opinii, i tylko bardziej go myli.
Andres F.,

ale dlaczego niektórzy mówią tutaj w przypadku C #, to nie jest historia? czytam kilka innych komentarzy i mówią w C # i C ++ historia (informacje o typach danych) jest inna i nawet procesor nie wykonuje obliczeń. Jakieś pomysły na ten temat?
user16307,
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.