[FUG-BR] RES: C/C++

Ricardo Nabinger Sanchez rnsanchez em gmail.com
Segunda Fevereiro 26 11:31:40 BRT 2007


On Mon, 26 Feb 2007 09:14:09 -0300
Nilson Debatin <nilson em forge.com.br> wrote:

> > Paulo, a função main nem sempre deve retorna um valor, somente se você
> > quiser, quando a função começa com void, significa que não retorna valor
> > nenhum, e ainda você falou que a função main DEVE retornar um valor
> > inteiro, isso também esta errado, a função pode retornar um char, float,
> > double, usigned float...e mais um monte...
> > 
> > Att. 
> 
> Sei que to uns dias atrasados mas quero ser mais um a frisar
> que você está viajando na maionese, a função main **SEMPRE**
> retorna um valor inteiro, você pode até fazer ela void só
> que nesse caso quando seu programa terminar ele VAI SIM 
> retornar um inteiro, o 0. Vai ser sempre sucesso, o que
> nem sempre é verdade dependendo do que o programa fará, se
> quiser andar mais na linha do correto, faça sempre ela int.

Pela semântica da linguagem "void main()" não retorna nada.  Se retornasse
algo, o compilador estaria fazendo algo errado.  E ainda assim, muito depende
da parceria entre compilador e o sistema operacional.  Os padrões querem
que os programas em C funcionem (ou pelo menos compilem) independente do
sistema operacional e compilador.  É difícil garantir isso na prática.

De volta ao main, ele retornará algo útil se o programador assim quiser, ou
então, durante a ligação do programa, o compilador decidirá pelo programador
o que será retornado.  Se o programador não especificou nada, então será
retornado o que estiver ao alcance da mão.

Quem quiser ler adiante, apresento alguns fatos que podem ser interessantes
apenas para curiosos.  Para os demais, basta saber que main é especial para a
fase de ligação do programa (momento de geração do executável), e espera-se
que ela retorne um inteiro para indicar o status de finalização do programa.



% cat main.c
void main() {}
% gcc main.c
main.c: In function `main':
main.c:1: warning: return type of 'main' is not `int'
%  ./a.out 
Exit 16

Claramente 16 não é zero.  O que houve afinal?

% objdump -d a.out
...
0804848c <main>:
 804848c:	55                   	push   %ebp
 804848d:	89 e5                	mov    %esp,%ebp
 804848f:	83 ec 08             	sub    $0x8,%esp
 8048492:	83 e4 f0             	and    $0xfffffff0,%esp
 8048495:	b8 00 00 00 00       	mov    $0x0,%eax  # eax = 0
 804849a:	83 c0 0f             	add    $0xf,%eax  # eax = 0xf
 804849d:	83 c0 0f             	add    $0xf,%eax  # eax = 0x1e
 80484a0:	c1 e8 04             	shr    $0x4,%eax  # eax = 1
 80484a3:	c1 e0 04             	shl    $0x4,%eax  # eax = 16
 80484a6:	29 c4                	sub    %eax,%esp  # esp -= 16
 80484a8:	c9                   	leave  
 80484a9:	c3                   	ret    
 80484aa:	90                   	nop    
 80484ab:	90                   	nop
...


Ainda assim, da onde veio esse 16?  A última instrução executada antes do
programa finalizar foi a do endereço 0x08048326, sendo que a entrada nesse
trecho se deu pelo endereço 0x08048350:

Disassembly of section .plt:
08048320 <.plt>:
 8048320:	ff 35 34 96 04 08    	pushl  0x8049634
 8048326:	ff 25 38 96 04 08    	jmp    *0x8049638
 804832c:	00 00                	add    %al,(%eax)
 804832e:	00 00                	add    %al,(%eax)
 8048330:	ff 25 3c 96 04 08    	jmp    *0x804963c
 8048336:	68 00 00 00 00       	push   $0x0
 804833b:	e9 e0 ff ff ff       	jmp    8048320 <_init+0x14>
 8048340:	ff 25 40 96 04 08    	jmp    *0x8049640
 8048346:	68 08 00 00 00       	push   $0x8
 804834b:	e9 d0 ff ff ff       	jmp    8048320 <_init+0x14>
 8048350:	ff 25 44 96 04 08    	jmp    *0x8049644
 8048356:	68 10 00 00 00       	push   $0x10
 804835b:	e9 c0 ff ff ff       	jmp    8048320 <_init+0x14>


O conteúdo dos registradores antes desta última chamada para biblioteca
externa era:
(gdb) info registers
eax            0x10     16
ecx            0x1      1
edx            0x10     16
ebx            0x1      1
esp            0xbfbfe744       0xbfbfe744
ebp            0xbfbfe778       0xbfbfe778
esi            0xbfbfe788       -1077942392
edi            0x2804e2a0       671408800
eip            0x8048326        0x8048326
eflags         0x282    642
cs             0x33     51
ss             0x3b     59
ds             0x3b     59
es             0x3b     59
fs             0x3b     59
gs             0x1b     27

Só que neste ponto, a função main já foi executada há muitas instruções
atrás, e não retornou nada pra ninguém.  Mas o ligador não sabe disso, e azar
do programador, pois ele vai ligar a crt0.o de qualquer jeito.  E na crt0.o
se assume que o topo da pilha (e registrador eax, em x86) contém o parâmetro
que será usado como valor de retorno.

Nesse exemplo, tanto a última coisa empilhada (8048356: push $0x10) quanto o
eax contêm 0x00000010, ou 16 em base decimal.  O byte menos significativo é
usado como valor de saída do programa, 0x10 (também 16 em decimal).


Usando um outro exemplo agora:
% cat tosco.c 
void main() { printf("12345678901234567890123456"); }
% gcc tosco.c 
tosco.c: In function `main':
tosco.c:1: warning: return type of 'main' is not `int'
% ./a.out | wc -c
      26
Exit 26


Nada a ver com o anterior.  O que houve desta vez?

080484f8 <main>:
 80484f8:       55                      push   %ebp
 80484f9:       89 e5                   mov    %esp,%ebp
 80484fb:       83 ec 08                sub    $0x8,%esp
 80484fe:       83 e4 f0                and    $0xfffffff0,%esp
 8048501:       b8 00 00 00 00          mov    $0x0,%eax
 8048506:       83 c0 0f                add    $0xf,%eax
 8048509:       83 c0 0f                add    $0xf,%eax
 804850c:       c1 e8 04                shr    $0x4,%eax
 804850f:       c1 e0 04                shl    $0x4,%eax
 8048512:       29 c4                   sub    %eax,%esp
 8048514:       83 ec 0c                sub    $0xc,%esp
 8048517:       68 a3 85 04 08          push   $0x80485a3
 804851c:       e8 6b fe ff ff          call   804838c <_init+0x24>
 8048521:       83 c4 10                add    $0x10,%esp
 8048524:       c9                      leave  
 8048525:       c3                      ret    
 8048526:       90                      nop    
 8048527:       90                      nop

O conteúdo dos registradores antes de abandonar o main era:
0x08048524 in main ()
(gdb) i r
eax            0x1a     26
ecx            0x0      0
edx            0x1a     26
ebx            0x1      1
esp            0xbfbfe720       0xbfbfe720
ebp            0xbfbfe738       0xbfbfe738
esi            0xbfbfe788       -1077942392
edi            0x2804e2a0       671408800
eip            0x8048524        0x8048524
eflags         0x282    642
cs             0x33     51
ss             0x3b     59
ds             0x3b     59
es             0x3b     59
fs             0x3b     59
gs             0x1b     27

Da onde veio esse 26?  Do printf, que retorna a quantidade de bytes escritos
no arquivo (stdout).  Se o programador vai usar ele não sabe, mas ele retorna
de qualquer jeito.  A execução continua, caindo no endereço 0x080483bc:

Disassembly of section .plt:
0804837c <.plt>:
 804837c:       ff 35 c8 96 04 08       pushl  0x80496c8
 8048382:       ff 25 cc 96 04 08       jmp    *0x80496cc
 8048388:       00 00                   add    %al,(%eax)
 804838a:       00 00                   add    %al,(%eax)
 804838c:       ff 25 d0 96 04 08       jmp    *0x80496d0
 8048392:       68 00 00 00 00          push   $0x0
 8048397:       e9 e0 ff ff ff          jmp    804837c <_init+0x14>
 804839c:       ff 25 d4 96 04 08       jmp    *0x80496d4
 80483a2:       68 08 00 00 00          push   $0x8
 80483a7:       e9 d0 ff ff ff          jmp    804837c <_init+0x14>
 80483ac:       ff 25 d8 96 04 08       jmp    *0x80496d8
 80483b2:       68 10 00 00 00          push   $0x10
 80483b7:       e9 c0 ff ff ff          jmp    804837c <_init+0x14>
 80483bc:       ff 25 dc 96 04 08       jmp    *0x80496dc
 80483c2:       68 18 00 00 00          push   $0x18
 80483c7:       e9 b0 ff ff ff          jmp    804837c <_init+0x14>

Antes de executar a última instrução do programa (a execução continua em
biblioteca externa, para encerrar de fato o programa), o conteúdo dos
registradores não tinha mudado muito:

0x08048382 in ?? ()
(gdb) i r
eax            0x1a     26
ecx            0x0      0
edx            0x1a     26
ebx            0x1      1
esp            0xbfbfe744       0xbfbfe744
ebp            0xbfbfe778       0xbfbfe778
esi            0xbfbfe788       -1077942392
edi            0x2804e2a0       671408800
eip            0x8048382        0x8048382
eflags         0x282    642
cs             0x33     51
ss             0x3b     59
ds             0x3b     59
es             0x3b     59
fs             0x3b     59
gs             0x1b     27

OK, mas e o conteúdo da pilha?

(gdb) x/20 0xbfbfe700
0xbfbfe700:     0x00000296      0x00000001      0xbfbfe738      0x08048521
0xbfbfe710:     0x080485a3      0x2806e200      0xbfbfe730      0x00000001
0xbfbfe720:     0xbfbfe788      0x2804e2a0      0xbfbfe748      0x28127641
0xbfbfe730:     0x00000010      0x00000001      0xbfbfe778      0x08048442
0xbfbfe740:     0x00000001      0x2806e000      0x00000018      0x0804844b
                                ^ topo
O 0x18 empilhado por último já foi usado, mas ainda dá pra ver ele na pilha.
Bom, não tem como ter certeza se ainda é ele, mas é uma coincidência bem
grande já que para o outro programa também dava pra ver o 0x10.

O que realmente faz sentido é que o programa sempre encerra com status 26, e
26 é o conteúdo do eax.  Só pode ser um truque específico do crt0.o, que
"viola" as convenções de chamada para x86, "esquecendo" de empilhar o código
de retorno.  Pra quê, se o programa está encerrando?


O que se aprende depois de toda essa trova?  Se um programa em C quer
retornar algo para o sistema, ele *precisa*:

	1- não ser void
	2- efetivamente retornar algo com return ou exit()

Ao contrário da sabedoria popular e dos padrões estabelecidos, é possível
gerar um programa que não seja "int main...":

% cat char.c 
char main() { return 123; }
% cat uint.c 
unsigned int main() { return 0x12345678; }
% cat voidp.c 
void * main() { return 0x12345678; }
% ./char 
Exit 123
% ./uint
Exit 120
% ./voidp 
Exit 120

Pra quem ficou curioso, 0x78 é 120 em base decimal, e é o byte menos
significativo na palavra 0x12345678.

-- 
Ricardo Nabinger Sanchez     <rnsanchez@{gmail.com,wait4.org}>
Powered by FreeBSD

  "Left to themselves, things tend to go from bad to worse."


Mais detalhes sobre a lista de discussão freebsd