Acreditem em mim: Eu também sou humano…

… e cometo erros grosseiros de tempos em tempos. O último foi tentar explicar para um amigo do Facebook a semântica da palavra reservada static, da linguagem C. Vou tentar colocar a cabeça no lugar e explicar direito aqui.

Como você pode supor static é o contrário de dynamic. Mas o significado de “dinâmico”, no contexto da declaração de símbolos, é diferente, dependendo de onde ele se encontra… Para começarmos, deixe-me definir aqui o conceito de “símbolo”, que vou usar muito no resto do texto: Um nome de variável, função, estrutura, ou qualquer outro “nome” é chamado de “símbolo”…

Por default, todos os símbolos globais são exportados para o linker pelo compilador. Isso permite que um módulo (um arquivo com extensão “.c”) declare alguma coisa que possa ser usada por outro módulo… Ao declarar algo assim:

int x;

Você está dizendo para o compilador alocar espaço para o símbolo x do tamanho de um inteiro (4 bytes). Mas, está dizendo também que esse símbolo pode ser “visto” por todos os módulos do seu programa… A declaração completa dessa variável é, por default:

extern auto signed int x;

A palavra reservada extern diz que o símbolo é exportado para o linker. “auto” diz que ele não é “volátil” e, claro, “signed” diz que deve ser interpretado como tendo sinal. Segue o tipo da variável (int) e seu nome. As palavras reservadas “extern”, “auto” e “signed” são assumidas por default (‘int’ também é, você poderia declarar essa variável como ‘a;’ se quisesse)… Por isso que a palavra reservada “unsigned” tem que ser usada para declarar inteiros não sinalizados, “volatile” para declarar variáveis “voláteis” e static para dizer ao compilador que o símbolo não deve ser exportado para o linker. Ou seja, ele é local ao escopo do módulo. Assim, perdemos o “dinamismo” da exportação do nome do símbolo…

Quando aos símbolos locais: Eles nunca são exportados para o linker, ou melhor, eles não são usados por outros módulos… Então o “dinamismo” muda de sentido: A palavra reservada “extern” perde o significado que era usado nos símbolos globais (exceto se for para declarar um protótipo de função dentro do escopo de uma função, mas mesmo isso já caiu em desuso), mesmo assim as palavras “auto” e “signed” continuam sendo assumidas por default… Os símbolos locais são assumidos como “dinâmicos” porque são alocados dinamicamente (na pilha ou em registradores) e desaparecem assim que a função termina…. Ao declarar um símbolo local como static estamos pedindo ao compilador que este símbolo deixe de ser “dinâmico”, ou seja, que o seu estado seja mantido entre as chamadas da função…

Uma variável local static é, então, uma variável global (ao módulo) acessível apenas à função onde foi declarada. E assim, deixo de usar a palavra “símbolo” uma porção de vezes (já tava ficando chato!).

As sessões .data e .bss:

Diferente do escopo ao qual a variável pertence, o compilador (e o linker) precisa decidir onde colocá-la. Uma variável sempre será alocada no segmento de dados… Mas, este segmento é dividido, logicamente, em dois pedaços ou sessões…

Os nomes .bss e .data, atribuídos às sessões que contém os dados são divisões do mesmo segmento de dados. A diferença é que todo o conteúdo da sessão .data é colocada na imagem binária gerada pelo linker. A sessão .bss, por sua vez, é criada em tempo de execução e não aparece na imagem binária!

Isso é facilmente observável no seguinte exemplo:

/* Variáveis 'extern' */
char a;
char b=1;

/* Variáveis locais a este módulo. */
static char c;
static char d=1;

int func(int x, int y)
{
  /* Variável local a esta função. */
  int e;

  /* Variável local ao módulo, mas acessível
     apenas por essa função! */
  static int f;

  f = x;
  e = f + y;

  return e;
}

Ao compilar e observar o arquivo objeto com o utilitário objdump:

$ gcc -c -o test.o test.c
$ objdump -t test.o

test.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*     0000000000000000 test.c
0000000000000000 l    d  .text     0000000000000000 .text
0000000000000000 l    d  .data     0000000000000000 .data
0000000000000000 l    d  .bss      0000000000000000 .bss
0000000000000000 l     O .bss      0000000000000001 c
0000000000000001 l     O .data     0000000000000001 d
0000000000000004 l     O .bss      0000000000000004 f.1730
0000000000000000 l    d  .note.GNU-stack    0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame 0000000000000000 .eh_frame
0000000000000000 l    d  .comment  0000000000000000 .comment
0000000000000001       O *COM*     0000000000000001 a
0000000000000000 g     O .data     0000000000000001 b
0000000000000000 g     F .text     0000000000000026 func

Note que as variáveis b e d foram colocadas na sessão .data porque foram inicializadas. As variáveis c e f.1730 foram colocadas na sessão .bss porquê não foram. Se a variável f (que é static) tivesse sido inicializada ela também estaria na sessão .data

O símbolo (“símbolo” de novo?) a é outra história… ele encontra-se num estado intermediário, já que pode estar definido em outro módulo. A declaração dele é extern por default, lembra?

Dando uma olhada nas sessões contidas no formato ELF, temos:

$ objdump -h test.o

test.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000002c  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  0000006c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000001  0000000000000000  0000000000000000  00000074  2**0
                  ALLOC
  3 .comment      0000002c  0000000000000000  0000000000000000  00000074  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000a0  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000a0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

Repare que a sessão .data é carregada (LOAD) e a .bss não é (não tem LOAD). Ainda… a sessão .data tem conteúdo (CONTENTS) e a sessão .bss não tem.

A regra geral é: Variáveis não inicializadas ou inicializadas com zero são sempre colocadas na sessão .bss… Isso porque, já que essa sessão não está contida na imagem binária (apenas o tamanho dela está!), então o código de pré-carga do seu executável alocará essa região no segmento de dados e a zerará completamente.

Mas, falei de “regra geral”, que pode ter alguma especificidade que a sobrepuje… De fato, fica à critério do compilador (ou do linker) pré-inicializar ou não arrays com inicializações parciais, como em:

int x[10] = { 1 };

Embora a especificação da linguagem diga que o valor variáveis não inicializadas sejam “dependentes de implementação”, é certo que essas serão colocadas na sessão .bss e automaticamente zeradas. Mas, não conte com isso, ok?

No exemplo acima, todos os itens do array estarão zerados, exceto por x[0]. Neste caso o compilador (ou o linker) pode escolher colocar os 40 bytes do array na imagem binária ou alocar 40 bytes do array durante a pré-carga, zerar todos os bytes modificar apenas os 4 bytes iniciais (o x[0])… Mas, note, isso nada tem a ver com o símbolo ser static ou não!

 

Anúncios