Os declaradores de tipo de dados como "int" e "char" são armazenados na RAM quando um programa C é executado?


74

Quando um programa C está em execução, os dados são armazenados na pilha ou na pilha. Os valores são armazenados nos endereços de RAM. Mas e os indicadores de tipo (por exemplo, intou char)? Eles também são armazenados?

Considere o seguinte código:

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

Eu li que A e 4 são armazenados nos endereços de RAM aqui. Mas ae quanto x? O mais confuso é: como a execução sabe que aé um caractere e xé um int? Quero dizer, é o inte charmencionado em algum lugar na RAM?

Digamos que um valor seja armazenado em algum lugar da RAM como 10011001; se eu sou o programa que executa o código, como vou saber se esse 10011001 é um charou um int?

O que não entendo é como o computador sabe, quando lê o valor de uma variável de um endereço como 10001, se é um intou char. Imagine clicar em um programa chamado anyprog.exe. Imediatamente o código começa a executar. Este arquivo executável inclui informações sobre se as variáveis ​​armazenadas são do tipo intou char?


24
Esta informação é totalmente perdida em tempo de execução. Você (e seu compilador) precisa ter certeza de que a memória será interpretada corretamente. Essa é a resposta que você procurava?
5gon12eder

4
Não faz. Como pressupõe que você sabe o que está fazendo, ele pega o que encontrar no endereço de memória que você forneceu e o grava no stdout. Se o que foi escrito corresponder a um caractere legível, ele aparecerá no console de alguém como um caractere legível. Se isso não corresponder, ele aparecerá como uma tagarelice ou, possivelmente, um caractere legível aleatório.
Robert Harvey

22
@ user16307 A resposta curta é que, nos idiomas estaticamente tipificados, sempre que você imprimir um caractere, o compilador produzirá um código diferente do que imprimiria um int. No tempo de execução, não há mais conhecimento de que xé um caractere, mas é o código de impressão de caracteres que é executado, porque foi o que o compilador selecionou.
Ixrec 5/08/2015

13
@ user16307 É sempre armazenada como a representação binária do número 65. A impressão como 65 ou A depende do código que o seu compilador produziu para imprimi-lo. Não há metadados próximos aos 65 que digam que é realmente um caractere ou um int (pelo menos, não em idiomas de tipo estaticamente como C).
Ixrec 5/08

2
A compreender plenamente os conceitos que você perguntar sobre aqui e implementá-las por si mesmo, você pode querer fazer um curso de compilador, por exemplo Coursera de um
mucaho

Respostas:


122

Para abordar a pergunta que você postou em vários comentários (que eu acho que você deve editar em sua postagem):

O que não entendo é como o computador sabe quando lê o valor e o endereço de uma variável, como 10001, se for int ou char. Imagine clicar em um programa chamado anyprog.exe. Imediatamente o código começa a executar. Este arquivo exe inclui informações sobre se as variáveis ​​são armazenadas como em ou char?

Então, vamos colocar algum código nele. Digamos que você escreva:

int x = 4;

E vamos supor que ele seja armazenado na RAM:

0x00010004: 0x00000004

A primeira parte é o endereço, a segunda parte é o valor. Quando seu programa (que é executado como código de máquina) é executado, tudo o que vê 0x00010004é o valor 0x000000004. Ele não 'sabe' o tipo desses dados e não sabe como eles devem ser usados.

Então, como o seu programa descobre a coisa certa a fazer? Considere este código:

int x = 4;
x = x + 5;

Temos uma leitura e uma gravação aqui. Quando seu programa lê xda memória, ele fica 0x00000004lá. E seu programa sabe adicionar 0x00000005a ele. E a razão pela qual seu programa 'sabe' que essa é uma operação válida é porque o compilador garante que a operação seja válida por meio de segurança de tipo. Seu compilador já verificou que você pode adicionar 4e 5juntar. Portanto, quando seu código binário é executado (o exe), ele não precisa fazer essa verificação. Ele apenas executa cada etapa às cegas, assumindo que tudo está OK (coisas ruins acontecem quando na verdade, não estão OK).

Outra maneira de pensar sobre isso é assim. Eu lhe dou esta informação:

0x00000004: 0x12345678

Mesmo formato de antes - endereço à esquerda, valor à direita. Que tipo é o valor? Neste ponto, você conhece tanta informação sobre esse valor quanto o seu computador quando está executando o código. Se eu lhe dissesse para adicionar 12743 a esse valor, você poderia fazê-lo. Você não tem idéia de quais serão as repercussões dessa operação em todo o sistema, mas adicionar dois números é algo em que você é realmente bom, para que possa fazê-lo. Isso torna o valor um int? Não necessariamente - tudo o que você vê são dois valores de 32 bits e o operador de adição.

Talvez um pouco da confusão esteja voltando aos dados. Se tiver-mos:

char A = 'a';

Como o computador sabe exibir ano console? Bem, existem muitos passos para isso. O primeiro é ir para Ao local s na memória e lê-lo:

0x00000004: 0x00000061

O valor hexadecimal aem ASCII é 0x61, portanto, o acima pode ser algo que você veria na memória. Então agora nosso código de máquina conhece o valor inteiro. Como ele sabe transformar o valor inteiro em um caractere para exibi-lo? Simplificando, o compilador certificou-se de executar todas as etapas necessárias para fazer essa transição. Mas o seu próprio computador (ou o programa / exe) não tem idéia de qual é o tipo desses dados. Esse valor de 32 bits pode ser qualquer coisa - int, charmetade de um double, um ponteiro, parte de uma matriz, parte de a string, parte de uma instrução, etc.


Aqui está uma breve interação que seu programa (exe) pode ter com o computador / sistema operacional.

Programa: Eu quero iniciar. Eu preciso de 20 MB de memória.

Sistema operacional: encontra 20 MB de memória livre que não estão em uso e os entrega

(A nota importante é que este poderia voltar quaisquer 20 MB livres de memória, eles nem sequer tem que ser contíguos. Neste ponto, o programa pode agora operar dentro da memória que ele tem sem falar com o OS)

Programa: Vou assumir que o primeiro ponto na memória é uma variável inteira de 32 bits x.

(O compilador garante que o acesso a outras variáveis ​​nunca toque esse ponto na memória. Não há nada no sistema que diga que o primeiro byte é variável xou que a variável xé um número inteiro. Uma analogia: você tem um saco. Você diz às pessoas que você só colocará bolas de cor amarela nessa sacola. Quando alguém mais tarde puxar algo da sacola, seria chocante que puxasse algo azul ou um cubo - algo deu terrivelmente errado. O mesmo vale para computadores: o seu Agora, o programa está assumindo que o primeiro ponto de memória é variável xe que é um número inteiro.Se algo mais for escrito sobre esse byte de memória ou se for considerado algo mais - algo horrível aconteceu. O compilador garante que esse tipo de coisa não seja acontecer)

Programa: Agora vou escrever 2nos quatro primeiros bytes em que suponho que xesteja.

Programa: quero adicionar 5 a x.

  • Lê o valor de X em um registro temporário

  • Adiciona 5 ao registro temporário

  • Armazena o valor do registro temporário de volta no primeiro byte, que ainda é assumido x.

Programa: Vou assumir que o próximo byte disponível é a variável char y.

Programa: Escreverei apara a variável y.

  • Uma biblioteca é usada para encontrar o valor de byte para a

  • O byte é gravado no endereço que o programa está assumindo y.

Programa: quero exibir o conteúdo de y

  • Lê o valor no segundo ponto de memória

  • Usa uma biblioteca para converter do byte para um caractere

  • Utiliza bibliotecas gráficas para alterar a tela do console (definindo pixels de preto para branco, rolando uma linha etc.)

(E continua a partir daqui)

O que você provavelmente está se incomodando é - o que acontece quando o primeiro lugar na memória não é mais x? ou o segundo não é mais y? O que acontece quando alguém lê xcomo um charou ycomo um ponteiro? Em suma, coisas ruins acontecem. Algumas dessas coisas têm um comportamento bem definido, e algumas têm um comportamento indefinido. O comportamento indefinido é exatamente isso - tudo pode acontecer, do nada, até travar o programa ou o sistema operacional. Mesmo um comportamento bem definido pode ser malicioso. Se eu posso mudar xpara um ponteiro para o meu programa e fazer com que seu programa o use como ponteiro, posso fazer com que seu programa comece a executar o meu programa - que é exatamente o que os hackers fazem. O compilador está lá para ajudar a garantir que não usamos int xcomo umstringe coisas dessa natureza. O código da máquina em si não está ciente dos tipos e fará apenas o que as instruções dizem. Também há uma grande quantidade de informações descobertas em tempo de execução: quais bytes de memória o programa pode usar? Será que xcomeçam no primeiro byte ou dia 12?

Mas você pode imaginar o quão horrível seria realmente escrever programas como este (e você pode, na linguagem assembly). Você começa 'declarando' suas variáveis ​​- diz a si mesmo que o byte 1 é x, o byte 2 é ye, ao escrever cada linha de código, carregando e armazenando registros, você (como humano) precisa se lembrar qual é xe qual uma é yporque o sistema não tem ideia. E você (como humano) precisa se lembrar de que tipos xe quais ysão, porque mais uma vez - o sistema não tem idéia.


Explicação surpreendente. Somente a parte que você escreveu "Como sabe transformar o valor inteiro em um caractere para exibi-lo? Simplificando, o compilador fez questão de colocar todas as etapas necessárias para fazer essa transição". ainda está nebuloso para mim. Vamos dizer que a CPU buscou 0x00000061 no registro de RAM. A partir deste ponto, você está dizendo que há outras instruções (no arquivo exe) que fazem essa transição para o que vemos na tela?
User16307

2
@ user16307 sim, existem instruções adicionais. Cada linha de código que você escreve pode potencialmente ser transformada em muitas instruções. Existem instruções para descobrir qual caractere usar, instruções para quais pixels modificar e para que cor eles mudam etc. Também há código que você realmente não vê. Por exemplo, usar std :: cout significa que você está usando uma biblioteca. Seu código para gravar no console pode ser apenas uma linha, mas as funções que você chama serão mais linhas e cada linha pode se transformar em muitas instruções da máquina.
Shaz

8
@ user16307 Otherwise how can console or text file outputs a character instead of int Porque existe uma sequência diferente de instruções para emitir o conteúdo de uma localização de memória como um número inteiro ou como caracteres alfanuméricos. O compilador conhece os tipos de variáveis ​​e escolhe a sequência apropriada de instruções em tempo de compilação e a registra no EXE.
Charles E. Grant

2
Eu encontraria uma frase diferente para "O código de byte em si", pois o código de byte (ou bytecode) geralmente se refere a uma linguagem intermediária (como Java Bytecode ou MSIL), que pode realmente armazenar esses dados para o tempo de execução aproveitar. Além disso, não está totalmente claro a que "código de bytes" se refere nesse contexto. Caso contrário, boa resposta.
Jpmc26

6
@ user16307 Tente não se preocupar com C ++ e C #. O que essas pessoas estão dizendo está muito acima do seu entendimento atual de como computadores e compiladores funcionam. Para os propósitos do que você está tentando entender, o hardware NÃO sabe nada sobre tipos, char ou int ou qualquer outra coisa. Quando você disse ao compilador que uma variável era um int, ele gerou código executável para manipular um local de memória COMO SE fosse um int. O local da memória em si não contém informações sobre tipos; é que seu programa decidiu tratá-lo como int. Esqueça tudo o que ouviu sobre informações sobre o tipo de tempo de execução.
Andrés F.

43

Eu acho que sua principal pergunta parece ser: "Se o tipo é apagado no tempo de compilação e não retido no tempo de execução, como o computador sabe se deve executar o código que o interpreta como um intou executar o código que o interpreta como um char? "

E a resposta é ... o computador não. No entanto, o compilador não sabe, e ele vai ter simplesmente colocar o código correto no binário em primeiro lugar. Se a variável fosse digitada como char, então o compilador não colocaria o código para tratá-lo como um intno programa, colocaria o código para tratá-lo como a char.

Não são motivos para reter o tipo em tempo de execução:

  • Digitação dinâmica: na digitação dinâmica, a verificação de tipo acontece no tempo de execução; portanto, obviamente, o tipo deve ser conhecido no tempo de execução. Como C não é digitado dinamicamente, os tipos podem ser apagados com segurança. (Observe que esse é um cenário muito diferente. Tipos dinâmicos e estáticos não são a mesma coisa e, em uma linguagem de digitação mista, você ainda pode apagar os tipos estáticos e manter apenas os tipos dinâmicos.)
  • Polimorfismo dinâmico: se você executar código diferente com base no tipo de tempo de execução, precisará manter o tipo de tempo de execução por perto. C não possui polimorfismo dinâmico (na verdade, não possui nenhum polimorfismo, exceto em alguns casos especiais codificados, por exemplo, o +operador), portanto, não precisa do tipo de tempo de execução por esse motivo. No entanto, novamente, o tipo de tempo de execução é algo diferente do tipo estático de qualquer maneira, por exemplo, em Java, você poderia apagar teoricamente os tipos estáticos e ainda manter o tipo de tempo de execução para polimorfismo. Observe também que, se você descentraliza e especializa o código de pesquisa de tipo e o coloca dentro do objeto (ou classe), também não precisa necessariamente do tipo de tempo de execução, por exemplo, C ++ vtables.
  • Reflexão em tempo de execução: se você permitir que o programa reflita sobre seus tipos em tempo de execução, obviamente precisará manter os tipos em tempo de execução. Você pode ver isso facilmente com Java, que mantém tipos de primeira ordem em tempo de execução, mas apaga argumentos de tipo para tipos genéricos em tempo de compilação, para que você possa refletir apenas no construtor de tipo ("tipo bruto"), mas não no argumento de tipo. Novamente, C não tem reflexão em tempo de execução, portanto, não precisa manter o tipo em tempo de execução.

O único motivo para manter o tipo em tempo de execução em C seria a depuração; no entanto, a depuração geralmente é feita com a fonte disponível e, em seguida, você pode simplesmente procurar o tipo no arquivo de origem.

O tipo Erasure é bastante normal. Isso não afeta a segurança do tipo: os tipos são verificados no momento da compilação, quando o compilador estiver convencido de que o programa é seguro para o tipo, os tipos não serão mais necessários (por esse motivo). Ele não afeta o polimorfismo estático (também conhecido como sobrecarga): depois que a resolução da sobrecarga é concluída e o compilador seleciona a sobrecarga correta, ele não precisa mais dos tipos. Os tipos também podem orientar a otimização, mas, novamente, depois que o otimizador seleciona suas otimizações com base nos tipos, ele não precisa mais delas.

A retenção de tipos no tempo de execução é necessária apenas quando você deseja fazer algo com os tipos no tempo de execução.

Haskell é uma das linguagens de tipo estaticamente mais estritas, mais rigorosas e com segurança de tipo, e os compiladores Haskell geralmente apagam todos os tipos. (A exceção é a passagem de dicionários de métodos para classes de tipos, acredito.)


3
Não! Por quê? Para que essas informações seriam necessárias? O compilador gera o código para ler a charno binário compilado. Ele não emite o código de um int, ele não emite o código para um byte, não saída o código para um ponteiro, ele simplesmente produz unicamente o código para um char. Não há decisões de tempo de execução sendo tomadas com base no tipo. Você não precisa do tipo. É completamente e totalmente irrelevante. Todas as decisões relevantes já foram tomadas em tempo de compilação.
Jörg W Mittag

2
Não existe. O compilador simplesmente coloca o código para imprimir um caractere no binário. Período. O compilador sabe que nesse endereço de memória existe char, portanto, ele coloca o código para imprimir um char no binário. Se o valor nesse endereço de memória, por algum motivo estranho, não for um caractere, então, bem, todo o inferno se abrirá. É basicamente assim que funciona toda uma classe de explorações de segurança.
Jörg W Mittag

2
Pense nisso: se a CPU soubesse de alguma forma sobre os tipos de dados dos programas, todo mundo no planeta teria que comprar uma nova CPU toda vez que alguém inventar um novo tipo. public class JoergsAwesomeNewType {};Vejo? Acabei de inventar um novo tipo! Você precisa comprar uma nova CPU!
Jörg W Mittag

9
Não. Não faz. O compilador sabe qual código ele deve colocar no binário. Não faz sentido manter essas informações por perto. Se você estiver imprimindo um int, o compilador colocará o código para imprimir um int. Se você estiver imprimindo um char, o compilador colocará o código para imprimir um char. Período. Mas é apenas um pouco de padrão. O código para imprimir um caractere interpretará o padrão de bits de uma certa maneira, o código para imprimir um int interpretará o bit de uma maneira diferente, mas não há como distinguir um padrão de bits que é um int e um padrão de bits que é um caractere, é uma sequência de bits.
Jörg W Mittag

2
@ user16307: "O arquivo exe não inclui informações sobre qual endereço é que tipo de dados?" Talvez. Se você compilar com dados de depuração, os dados de depuração incluirão informações sobre nomes, endereços e tipos de variáveis. E às vezes esses dados de depuração são armazenados no arquivo .exe (como um fluxo binário). Mas não faz parte do código executável e não é usado pelo próprio aplicativo, apenas por um depurador.
Ben Voigt

12

O computador não "sabe" quais endereços são o quê, mas o conhecimento do que é o que está inserido nas instruções do seu programa.

Quando você escreve um programa C que grava e lê uma variável char, o compilador cria um código de montagem que grava esses dados em algum lugar como char, e há outro código em outro lugar que lê um endereço de memória e o interpreta como char. A única coisa que une essas duas operações é a localização desse endereço de memória.

Quando chega a hora de ler, as instruções não dizem "ver que tipo de dados existe", apenas dizem algo como "carregar essa memória como um flutuador". Se o endereço a ser lido tiver sido alterado ou algo sobrescrever essa memória por algo que não seja um float, a CPU simplesmente carregará essa memória como um float de qualquer maneira, e todos os tipos de coisas estranhas podem acontecer como resultado.

Tempo ruim de analogia: imagine um armazém de remessas complicado, onde o armazém é a memória e as pessoas que escolhem as coisas são a CPU. Uma parte do 'programa' do armazém coloca vários itens na prateleira. Outro programa vai e pega itens do armazém e os coloca em caixas. Quando são retirados, não são verificados, apenas vão para a lixeira. Todo o armazém funciona com tudo funcionando em sincronia, com os itens certos sempre no lugar certo, na hora certa; caso contrário, tudo falha, como em um programa real.


como você explicaria se a CPU encontrar 0x00000061 em um registro e buscá-lo; e imagine que o programa do console suponha que isso seja um caractere não int. você quer dizer que nesse arquivo exe existem alguns códigos de instrução que sabem que o endereço 0x00000061 é um caractere e converte em um caractere usando a tabela ASCII?
user16307

7
Observe que "tudo falha" é realmente o melhor cenário. "Coisas estranhas acontecem" é o segundo melhor cenário, "coisas sutilmente estranhas acontecem" é ainda pior, e o pior caso é "coisas acontecem nas suas costas que alguém intencionalmente manipulou para acontecer do jeito que elas querem", aka uma exploração de segurança.
Jörg W Mittag

@ user16307: O código no programa diz ao computador para buscar esse endereço e exibi-lo de acordo com a codificação que estiver sendo usada. Se esses dados no local da memória são caracteres ASCII ou lixo completo, o computador não está preocupado. Outra coisa foi responsável por configurar esse endereço de memória para ter os valores esperados. Eu acho que pode ser útil tentar alguma programação de montagem.
Whatsisname

1
@ JörgWMittag: de fato. Pensei em mencionar um estouro de buffer como exemplo, mas decidi que isso tornaria as coisas mais confusas.
Whatsisname

@ user16307: O que exibe dados na tela é um programa. No unixen tradicional, é um terminal (um software que emula o terminal serial DEC VT100 - um dispositivo de hardware com um monitor e teclado que exibe o que entra no modem para o monitor e envia o que é digitado no teclado para o modem). No DOS, é o DOS (na verdade, o modo de texto da sua placa VGA, mas vamos ignorar isso) e no Windows, o command.com. Seu programa não sabe que está realmente imprimindo strings, apenas imprimindo uma sequência de bytes (números).
Slebetman

8

Não faz. Uma vez que C é compilado no código da máquina, a máquina vê apenas alguns bits. Como esses bits são interpretados depende de quais operações estão sendo executadas neles, em oposição a alguns metadados adicionais.

Os tipos digitados no seu código-fonte são apenas para o compilador. Ele usa o tipo que você diz que os dados devem ter e, na melhor das hipóteses, tenta garantir que esses dados sejam usados ​​apenas de maneiras que façam sentido. Depois que o compilador faz o melhor trabalho possível na verificação da lógica do seu código-fonte, ele o converte em código de máquina e descarta os dados do tipo, porque o código de máquina não tem como representá-lo (pelo menos na maioria das máquinas) .


O que não entendo é como o computador sabe quando lê o valor e o endereço de uma variável, como 10001, se for int ou char. Imagine clicar em um programa chamado anyprog.exe. Imediatamente o código começa a executar. Este arquivo exe inclui informações sobre se as variáveis ​​são armazenadas como em ou char? -
user16307

@ user16307 Não, não há informações extras sobre se algo é int ou char. Acrescentarei algumas coisas de exemplo mais tarde, supondo que ninguém mais me supere.
precisa saber é o seguinte

1
@ user16307: O arquivo exe contém essas informações indiretamente. O processador que está executando o programa não se importa com os tipos usados ​​ao escrever o programa, mas grande parte pode ser deduzida das instruções usadas para acessar os vários locais de memória.
Bart van Ingen Schenau 5/08/15

@ user16307 Na verdade, existem algumas informações extras. Os arquivos exe sabem que um número inteiro tem 4 bytes; portanto, quando você escreve "int a", o compilador reserva 4 bytes para a variável a e, portanto, pode calcular o endereço de uma e das outras variáveis ​​depois.
Esben Skov Pedersen

1
@ user16307 não existe diferença prática (além do tamanho do tipo) diferença entre int a = 65e char b = 'A'depois que o código for compilado.

6

A maioria dos processadores fornece instruções diferentes para trabalhar com dados de tipos diferentes; portanto, as informações de tipo geralmente são "inseridas" no código de máquina gerado. Não há necessidade de armazenar metadados de tipo adicionais.

Alguns exemplos concretos podem ajudar. O código de máquina abaixo foi gerado usando o gcc 4.1.2 em um sistema x86_64 executando o SuSE Linux Enterprise Server (SLES) 10.

Suponha o seguinte código-fonte:

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

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

Aqui está a descrição do código de montagem gerado correspondente à fonte acima (usando gcc -S), com comentários adicionados por mim:

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

Há algumas coisas extras a seguir ret, mas não são relevantes para a discussão.

%eaxé um registro de dados de uso geral de 32 bits. %rspé um registro de 64 bits reservado para salvar o ponteiro da pilha , que contém o endereço da última coisa colocada na pilha. %rbpé um registro de 64 bits reservado para salvar o ponteiro de quadro , que contém o endereço do quadro de pilha atual . Um quadro de pilha é criado na pilha quando você insere uma função e reserva espaço para os argumentos e variáveis ​​locais da função. Argumentos e variáveis ​​são acessados ​​usando deslocamentos do ponteiro do quadro. Nesse caso, a memória da variável xé de 12 bytes "abaixo" do endereço armazenado %rbp.

No código acima, copiamos o valor inteiro de x(1, armazenado em -12(%rbp)) no registro %eaxusando a movlinstrução, que é usada para copiar palavras de 32 bits de um local para outro. Em seguida addl, chamamos , que adiciona o valor inteiro de y(armazenado em -8(%rbp)) ao valor já existente %eax. Em seguida, salvamos o resultado em -4(%rbp), o que é z.

Agora vamos mudar isso, então estamos lidando com doublevalores em vez de intvalores:

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

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

Correr gcc -Snovamente nos dá:

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

Várias diferenças. Em vez de movle addl, usamos movsde addsd(atribuímos e adicionamos flutuadores de precisão dupla). Em vez de armazenar valores provisórios %eax, usamos %xmm0.

É isso que quero dizer quando digo que o tipo é "incorporado" ao código da máquina. O compilador simplesmente gera o código de máquina certo para lidar com esse tipo específico.


4

Historicamente , C considerava a memória composta por vários grupos de slots numerados do tipounsigned char(também chamado de "byte", embora nem sempre seja 8 bits). Qualquer código que usasse qualquer coisa armazenada na memória precisaria saber em qual slot ou slots as informações foram armazenadas e saber o que deve ser feito com as informações existentes [por exemplo "interpretar os quatro bytes começando no endereço 123: 456 como um arquivo de 32 bits. valor de ponto flutuante "ou" armazena os 16 bits mais baixos da quantidade calculada mais recentemente em dois bytes, começando no endereço 345: 678]. A própria memória não saberia nem se importaria com o que os valores armazenados nos slots de memória "significavam". Se o código tentasse escrever memória usando um tipo e lê-lo como outro, os padrões de bits armazenados pela gravação seriam interpretados de acordo com as regras do segundo tipo, com quaisquer consequências que possam resultar.

Por exemplo, se o código fosse armazenado 0x12345678em um de 32 bits unsigned inte, em seguida, tentasse ler dois valores consecutivos de 16 bits unsigned intdo endereço e do acima, dependendo da metade da qual unsigned intestava armazenada, o código pode ler os valores 0x1234 e 0x5678 ou 0x5678 e 0x1234.

O padrão C99, no entanto, não exige mais que a memória se comporte como um monte de slots numerados que nada sabem sobre o que seus padrões de bits representam . É permitido que um compilador se comporte como se os slots de memória estivessem cientes dos tipos de dados armazenados neles, e só permitirá que dados gravados usando qualquer tipo que unsigned charnão sejam lidos usando o tipo unsigned charou o mesmo tipo como foram gravados. com; os compiladores também podem se comportar como se os slots de memória tivessem o poder e a inclinação de corromper arbitrariamente o comportamento de qualquer programa que tente acessar a memória de uma maneira contrária a essas regras.

Dado:

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

algumas implementações podem imprimir 0x1234 e outras podem imprimir 0x5678, mas sob o C99 Standard, seria legal para uma implementação imprimir "REGRAS FRINK!" ou faça qualquer outra coisa, com a teoria de que seria legal para os locais de memória aque incluem incluir hardware que registre o tipo usado para gravá-los e que esse hardware responda a uma tentativa de leitura inválida de qualquer forma, inclusive causando "REGRAS FRINK!" para ser produzido.

Observe que não importa se existe algum hardware - o fato de que esse hardware possa existir legalmente torna legal para os compiladores gerar código que se comporta como se estivesse sendo executado em um sistema assim. Se o compilador puder determinar que um determinado local de memória será gravado como um tipo e lido como outro, poderá fingir que está sendo executado em um sistema cujo hardware possa fazer essa determinação e responder com qualquer grau de capricho que o autor do compilador considerar adequado. .

O objetivo desta regra era permitir que os compiladores que sabiam que um grupo de bytes que detinha um valor de algum tipo mantinham um valor específico em algum ponto no tempo e que nenhum valor desse mesmo tipo havia sido gravado desde então, inferir que esse grupo de bytes ainda manteria esse valor. Por exemplo, um processador leu um grupo de bytes em um registro e, posteriormente, desejou usar as mesmas informações novamente enquanto ainda estavam no registro, o compilador poderia usar o conteúdo do registro sem precisar reler o valor da memória. Uma otimização útil. Nos primeiros dez anos da regra, violá-la geralmente significa que, se uma variável for escrita com um tipo diferente daquele usado para lê-la, a gravação poderá ou não afetar o valor lido. Esse comportamento pode em alguns casos ser desastroso, mas em outros casos pode ser inofensivo,

Por volta de 2009, no entanto, os autores de alguns compiladores como o CLANG determinaram que, como o Padrão permite que os compiladores façam o que quiserem nos casos em que a memória é escrita usando um tipo e lida como outro, os compiladores devem inferir que os programas nunca receberão informações que possam fazer com que isso ocorra. Como o Padrão diz que o compilador pode fazer o que quiser quando essa entrada inválida é recebida, código que só teria efeito nos casos em que o Padrão não impõe nenhum requisito pode (e na opinião de alguns autores do compilador) deve ser omitido como irrelevante. Isso altera o comportamento das violações de aliasing de serem como memória que, dada uma solicitação de leitura, pode arbitrariamente retornar o último valor gravado usando o mesmo tipo que uma solicitação de leitura ou qualquer valor mais recente gravado usando outro tipo,


1
Mencionando um comportamento indefinido quando o tipo de poda para alguém que não entende como não há RTTI parece um contra-senso
Cole Johnson

@ColeJohnson: É uma pena que não exista um nome formal ou padrão para o dialeto C suportado por 99% dos compiladores anteriores a 2009, uma vez que, do ponto de vista do ensino e do prático, eles devem ser considerados idiomas fundamentalmente diferentes. Como o mesmo nome é dado ao dialeto que desenvolveu um número de comportamentos previsíveis e otimizáveis ​​ao longo de 35 anos, ao dialeto que descarta esses comportamentos com o objetivo de otimização, é difícil evitar confusão ao falar sobre coisas que funcionam de maneira diferente neles. .
Super8

Historicamente, o C era executado nas máquinas Lisp que não permitiam jogar tão livremente com tipos. Tenho certeza de que muitos dos "comportamentos previsíveis e otimizáveis" vistos 30 anos atrás simplesmente não funcionavam em lugar algum, além do BSD Unix no VAX.
prosfilaes

@prosfilaes: Talvez "99% dos compiladores usados ​​de 1999 a 2009" sejam mais precisos? Mesmo quando os compiladores tinham opções para algumas otimizações inteiras bastante agressivas, eram exatamente isso - opções. Não sei se já vi um compilador antes de 1999 que não tinha um modo que não garantisse que, dada int x,y,z;a expressão x*y > z, nada faria além de retornar 1 ou 0 ou onde as violações de aliasing teriam algum efeito. diferente de permitir que o compilador retorne arbitrariamente um valor antigo ou novo.
Supercat

1
... de onde unsigned charvieram os valores usados ​​para construir um tipo ". Se um programa decompor um ponteiro em um unsigned char[], mostre brevemente seu conteúdo hexadecimal na tela e, em seguida, apague o ponteiro, o unsigned char[], e depois aceite alguns números hexadecimais do teclado, copie-os novamente para um ponteiro e, em seguida, desreferencie esse ponteiro , o comportamento seria bem definido no caso em que o número digitado correspondesse ao número exibido.
Supercat

3

Em C, não é. Outros idiomas (por exemplo, Lisp, Python) têm tipos dinâmicos, mas C é de tipo estatístico. Isso significa que seu programa deve saber que tipo de dado os dados devem interpretar adequadamente como um caractere, um número inteiro etc.

Normalmente, o compilador cuida disso para você e, se você fizer algo errado, receberá um erro em tempo de compilação (ou aviso).


O que não entendo é como o computador sabe quando lê o valor e o endereço de uma variável, como 10001, se for int ou char. Imagine clicar em um programa chamado anyprog.exe. Imediatamente o código começa a executar. Este arquivo exe inclui informações sobre se as variáveis ​​são armazenadas como em ou char? -
user16307

1
@ user16307 Essencialmente não, todas essas informações são completamente perdidas. Cabe ao código da máquina ser projetado bem o suficiente para fazer seu trabalho corretamente, mesmo sem essas informações. Tudo o que o computador se importa é que haja oito bits seguidos no endereço 10001. É o seu trabalho ou o do compilador , dependendo do caso, acompanhar coisas assim manualmente enquanto escreve o código da máquina ou montagem.
Panzercrisis

1
Observe que a digitação dinâmica não é o único motivo para reter tipos. O Java é digitado estaticamente, mas ainda deve reter os tipos, porque permite refletir dinamicamente sobre o tipo. Além disso, possui polimorfismo de tempo de execução, ou seja, despacho de método com base no tipo de tempo de execução, para o qual também precisa do tipo. O C ++ coloca o código de despacho do método no próprio objeto (ou melhor, na classe); portanto, ele não precisa do tipo em algum sentido (embora, claro, a vtable seja, em algum sentido, parte do tipo, portanto, realmente pelo menos parte do o tipo é mantido), mas em Java, o código de despacho do método é centralizado.
Jörg W Mittag

olha a minha pergunta que escrevi "quando um programa C é executado?" Não são armazenados indiretamente no arquivo exe entre os códigos de instrução e, eventualmente, ocorrem na memória? Escrevo isso para você novamente: se a CPU encontrar 0x00000061 em um registro e buscá-lo; e imagine que o programa do console suponha que isso seja um caractere não int. existe nesse arquivo exe (máquina / código binário) alguns códigos de instrução que sabem que o endereço 0x00000061 é um caractere e converte em um caractere usando a tabela ASCII? Se sim, isso significa que os identificadores de caracteres int estão indiretamente no binário ???
user16307

Se o valor for 0x61 e for declarado como um caractere (ou seja, 'a') e você chamar uma rotina para exibi-lo, haverá [eventualmente] uma chamada do sistema para exibir esse caractere. Se você o declarou como int e chama a rotina de exibição, o compilador saberá gerar código para converter 0x61 (decimal 97) na sequência ASCII 0x39, 0x37 ('9', '7'). Conclusão: o código gerado é diferente porque o compilador sabe tratá-los de maneira diferente.
Mike Harris

3

Você precisa distinguir entre compiletimee runtimepor um lado codeee datapor outro lado.

Do ponto de vista da máquina, não há diferença entre o que você chama codeou instructionso que chama data. Tudo se resume a números. Mas algumas seqüências - o que chamaríamos code- fazem algo que consideramos útil, outras simplesmente crasha máquina.

O trabalho realizado pela CPU é um loop simples de 4 etapas:

  • Buscar "dados" de um determinado endereço
  • Decodifique a instrução (ou seja, "interprete" o número como um instruction)
  • Leia um endereço eficaz
  • Executar e armazenar resultados

Isso é chamado de ciclo de instruções .

Eu li que A e 4 são armazenados nos endereços de RAM aqui. Mas e quanto aex?

ae xsão variáveis, que são espaços reservados para os endereços, onde o programa pode encontrar o "conteúdo" das variáveis. Portanto, sempre que a variável aé usada, existe efetivamente o endereço do conteúdo ausado.

O mais confuso é: como a execução sabe que a é um caractere x é um int?

A execução não conhece nada. Pelo que foi dito na introdução, a CPU apenas busca dados e interpreta esses dados como instruções.

A função printf foi projetada para "saber", que tipo de entrada você está inserindo nela, ou seja, seu código resultante fornece as instruções corretas sobre como lidar com um segmento de memória especial. Obviamente, é possível gerar uma saída sem sentido: usando um endereço em que nenhuma string é armazenada junto com "% s" printf()resultará em uma saída sem sentido interrompida apenas por um local de memória aleatória, onde está um 0 ( \0).

O mesmo vale para o ponto de entrada de um programa. Sob o C64, era possível colocar seus programas em (quase) todos os endereços conhecidos. Os programas de montagem foram iniciados com uma instrução chamada sysseguida por um endereço: sys 49152era um local comum para colocar seu código de montagem. Mas nada o impediu de carregar, por exemplo, dados gráficos 49152, resultando em uma pane na máquina após "iniciar" a partir deste ponto. Nesse caso, o ciclo de instruções começou com a leitura de "dados gráficos" e a tentativa de interpretá-lo como "código" (o que, é claro, não fazia sentido); os efeitos foram surpreendentes;)

Digamos que um valor seja armazenado em algum lugar da RAM como 10011001; se eu sou o programa que executa o código, como vou saber se esse 10011001 é um char ou um int?

Como dito: O "contexto" - isto é, as instruções anteriores e seguintes - ajuda a tratar os dados da maneira que queremos. Do ponto de vista da máquina, não há diferença em nenhum local da memória. inte charé apenas vocabulário, o que faz sentido compiletime; durante runtime(no nível da montagem), não há charou int.

O que não entendo é como o computador sabe, quando lê o valor de uma variável de um endereço como 10001, se é int ou char.

O computador não sabe nada. O programador faz. O código compilado gera o contexto , necessário para gerar resultados significativos para humanos.

Esse arquivo executável inclui informações sobre se as variáveis ​​armazenadas são do tipo int ou char

Sim e Não . A informação, se é um intou a charé perdida. Mas, por outro lado, o contexto (as instruções que informam, como lidar com os locais da memória, onde os dados são armazenados) é preservado; tão implicitamente sim, a "informação" está implicitamente disponível.


Boa distinção entre tempo de compilação e tempo de execução.
Michael Blackburn

2

Vamos manter essa discussão apenas na linguagem C.

O programa ao qual você está se referindo está escrito em uma linguagem de alto nível como C. O computador entende apenas a linguagem de máquina. Linguagens de nível superior dão ao programador a capacidade de expressar a lógica de uma maneira mais humana, que é então traduzida em código de máquina que o microprocessador pode decodificar e executar. Agora vamos discutir o código que você mencionou:

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

Vamos tentar analisar cada parte:

char / int são conhecidos como tipos de dados. Eles dizem ao compilador para alocar memória. No caso char, serão 1 byte e int2 bytes. (Observe que esse tamanho de memória depende novamente do microprocessador).

a / x são conhecidos como identificadores. Agora, estes são os nomes "amigáveis" dados aos locais de memória na RAM.

= diz ao compilador para armazenar 'A' no local da memória ae 4 no local da memória x.

Portanto, os identificadores de tipo de dados int / char são usados ​​apenas pelo compilador e não pelo microprocessador durante a execução do programa. Portanto, eles não são armazenados na memória.


ok identificadores de tipo de dados int / char não são armazenados diretamente na memória como variáveis, mas eles não são armazenados indiretamente no arquivo exe entre os códigos de instrução e, eventualmente, ocorrem na memória? Escrevo isso para você novamente: se a CPU encontrar 0x00000061 em um registro e buscá-lo; e imagine que o programa do console suponha que isso seja um caractere não int. existe nesse arquivo exe (máquina / código binário) alguns códigos de instrução que sabem que o endereço 0x00000061 é um caractere e converte em um caractere usando a tabela ASCII? Se sim, isso significa que os identificadores de caracteres int estão indiretamente no binário ???
user16307

Não para a CPU, todos os seus números. Para o seu exemplo específico, a impressão no console é não depende se a variável é char ou int. Atualizarei minha resposta com o fluxo detalhado de como o programa de alto nível é convertido em linguagem de máquina até a execução do programa.
prasad

2

Minha resposta aqui é um pouco simplificada e se referirá apenas a C.

Não, as informações de tipo não são armazenadas no programa.

intou charnão são indicadores de tipo para a CPU; somente para o compilador.

O exe criado pelo compilador terá instruções para manipular ints se a variável foi declarada como um int. Da mesma forma, se a variável foi declarada como a char, o exe conterá instruções para manipular a char.

Em 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;
}

Este programa imprimirá sua mensagem, uma vez que os chare inttêm os mesmos valores na RAM.

Agora, se você está se perguntando como printfgerencia a saída 65de um inte Apara um char, é porque você precisa especificar na "cadeia de formato" como printfdeve tratar o valor .
(Por exemplo, %csignifica tratar o valor como a chare %dsignifica tratar o valor como um número inteiro; mesmo valor, de qualquer maneira.)


2
Eu esperava que alguém usasse um exemplo usando printf. @OP: int a = 65; printf("%c", a)será exibido 'A'. Por quê? Porque o processador não se importa. Para isso, tudo o que vê são bits. Seu programa disse ao processador para armazenar 65 (coincidentemente o valor 'A'em ASCII) ae depois gerar um caractere, o que é bom. Por quê? Porque não se importa.
Cole Johnson

mas por que alguns dizem aqui no caso de C #, não é a história? Eu li alguns outros comentários e eles dizem que em C # e C ++ a história (informações sobre tipos de dados) é diferente e até a CPU não faz a computação. Alguma idéia sobre isso?
user16307

@ user16307 Se a CPU não calcular, o programa não está em execução. :) Quanto ao C #, não sei, mas acho que minha resposta também se aplica a ele. Quanto ao C ++, eu sei que minha resposta se aplica lá.
usar o seguinte código

0

No nível mais baixo, na CPU física real, não há tipos (ignorando as unidades de ponto flutuante). Apenas padrões de bits. Um computador funciona manipulando padrões de bits, muito, muito rápido.

Isso é tudo o que a CPU faz, tudo o que pode fazer. Não existe int, nem char.

x = 4 + 5

Será executado como:

  1. Carregue 00000100 no registro 1
  2. Carregar 00000101 no registro 2
  3. IAdicione o registro 1 ao registro 2 e armazene-o no registro 1

A instrução iadd aciona o hardware que se comporta como se os registros 1 e 2 fossem números inteiros. Se eles não representam números inteiros, todos os tipos de coisas podem dar errado mais tarde. O melhor resultado é geralmente falhar.

Cabe ao compilador escolher a instrução correta com base nos tipos fornecidos na fonte, mas no código de máquina real executado pela CPU, não há tipos em nenhum lugar.

editar: Observe que o código de máquina real não menciona de fato 4, 5 ou número inteiro em qualquer lugar. são apenas dois padrões de bits e uma instrução que usa dois padrões de bits, assume que são ints e os soma.


0

Resposta curta, o tipo é codificado nas instruções da CPU que o compilador gera.

Embora as informações sobre o tipo ou tamanho das informações não sejam armazenadas diretamente, o compilador controla essas informações ao acessar, modificar e armazenar valores nessas variáveis.

como a execução sabe que a é um caractere x é um int?

Não, mas quando o compilador produz o código de máquina que conhece. Um inte um charpodem ser de tamanhos diferentes. Em uma arquitetura em que um caractere é do tamanho de um byte e um int é de 4 bytes, a variável xnão está no endereço 10001, mas também no 10002, 10003 e 10004. Quando o código precisa carregar o valor xem um registro da CPU, Ele usa a instrução para carregar 4 bytes. Ao carregar um caracter, ele usa a instrução para carregar 1 byte.

Como escolher qual das duas instruções? O compilador decide durante a compilação, isso não é feito em tempo de execução após a inspeção dos valores na memória.

Observe também que os registros podem ter tamanhos diferentes. Nas CPUs Intel x86, o EAX tem 32 bits de largura, metade dele é AX, que é 16, e o AX é dividido em AH e AL, ambos com 8 bits.

Portanto, se você deseja carregar um número inteiro (em CPUs x86), use a instrução MOV para números inteiros. Para carregar um caractere, use a instrução MOV para caracteres. Ambos são chamados de MOV, mas eles têm códigos op diferentes. Efetivamente sendo duas instruções diferentes. O tipo da variável é codificado na instrução a ser usada.

O mesmo acontece com outras operações. Existem muitas instruções para executar a adição, dependendo do tamanho dos operandos e mesmo se eles estão assinados ou não. Veja https://en.wikipedia.org/wiki/ADD_(x86_instruction), que lista diferentes adições possíveis.

Digamos que um valor seja armazenado em algum lugar da RAM como 10011001; se eu sou o programa que executa o código, como vou saber se esse 10011001 é um caractere ou um int

Primeiro, um caractere seria 10011001, mas um int seria 00000000 00000000 00000000 10011001, porque são de tamanhos diferentes (em um computador com os mesmos tamanhos mencionados acima). Mas vamos considerar o caso de signed charvs unsigned char.

O que é armazenado em um local de memória pode ser interpretado da maneira que você desejar. Parte das responsabilidades do compilador C é garantir que o que é armazenado e lido de uma variável seja feito de maneira consistente. Portanto, não é que o programa saiba o que está armazenado em um local de memória, mas que concorda de antemão que sempre lerá e escreverá o mesmo tipo de coisas lá. (sem contar coisas como tipos de transmissão).


mas por que alguns dizem aqui no caso de C #, não é a história? Eu li alguns outros comentários e eles dizem que em C # e C ++ a história (informações sobre tipos de dados) é diferente e até a CPU não faz a computação. Alguma idéia sobre isso?
user16307

0

mas por que alguns dizem aqui no caso de C #, não é a história? Eu li alguns outros comentários e eles dizem que em C # e C ++ a história (informações sobre tipos de dados) é diferente e até a CPU não faz a computação. Alguma idéia sobre isso?

Em linguagens verificadas por tipo como C #, a verificação de tipo é feita pelo compilador. O código benji escreveu:

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;
}

Simplesmente se recusaria a compilar. Da mesma forma, se você tentasse multiplicar uma string e um número inteiro (eu diria add, mas o operador '+' está sobrecarregado com concatenação de strings e pode funcionar).

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

O compilador simplesmente se recusaria a gerar código de máquina a partir desse C #, não importando o quanto sua string fosse aceita.


-4

As outras respostas estão corretas, pois essencialmente todos os dispositivos de consumo que você encontrará não armazenam informações de tipo. No entanto, houve vários projetos de hardware no passado (e atualmente, em um contexto de pesquisa) que usam uma arquitetura marcada - eles armazenam os dados e o tipo (e possivelmente outras informações também). Isso incluiria com mais destaque as máquinas Lisp .

Lembro-me vagamente de ouvir sobre uma arquitetura de hardware projetada para programação orientada a objetos que tinha algo semelhante, mas não consigo encontrá-lo agora.


3
A pergunta afirma especificamente que está se referindo à linguagem C (não Lisp), e a linguagem C não armazena metadados variáveis. Embora seja certamente possível para uma implementação C fazer isso, como o padrão não a proíbe, na prática isso nunca acontece. Se você tem exemplos relevantes para a questão, forneça citações específicas e fornecer referências que se relacionam com a linguagem C .

Bem, você poderia escrever um compilador C para uma máquina Lisp, mas ninguém usa máquinas Lisp hoje em dia. A arquitetura orientada a objetos era Rekursiv , a propósito.
18719 Nathan Ringo

2
Eu acho que essa resposta não é útil. Isso complica as coisas muito além do nível atual de entendimento do OP. É claro que o OP não entende o modelo básico de execução de uma CPU + RAM e como um compilador converte a fonte simbólica de alto nível em um binário executável. A memória marcada, RTTI, Lisp, etc, está muito além do que o solicitante precisa saber na minha opinião e apenas o confundirá mais.
Andrés F.

mas por que alguns dizem aqui no caso de C #, não é a história? Eu li alguns outros comentários e eles dizem que em C # e C ++ a história (informações sobre tipos de dados) é diferente e até a CPU não faz a computação. Alguma idéia sobre isso?
user16307
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.