[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