sexta-feira, 25 de maio de 2007

Sua Aplicação Cairá ... Mas irá se recuperar ... (Pt.:2)


Como eu havia dito num post anterior, uma hora dessas eu iria falar sobre os erros de violação de acesso que acontecem em tempo de execução e que na maioria dos casos, não são capturados pelo compilador do Delphi.

Repetindo algumas palavras do post anterior, os erros de violação de acesso são os erros mais frustrantes da programação Windows. Os erros de violação são divididos em erros de hardware e erros de software e, dos erros de software, estes estão divididos entre erros em tempo de compilação e erros em tempo de execução.

Os erros em tempo de execução são o tipo de erros que mais acontecem nas aplicações Windows.

Violações de acesso normalmente se manifestam com uma ação bem específica na aplicação. É importante pensar sobre a maneira como o usuário "acionou" o problema pela primeira vez, e a partir daí, trabalhar para a resolução do mesmo.

Uma maneira fácil de descobrir de onde estão vindo os access violations é utilizando arquivos de mapeamento do código binário, ou arquivos .map. No Delphi, você pode fazer isto utilizando a diretiva {$D}, que diz para o compilador criar o arquivo de mapeamento para aquela unit. Considere no caso de debugar uma violação de acesso, utilizar esta diretiva em todas as units pertencentes ao projeto. Uma desvantagem do uso deste processo é que as informações de depuração são armazenadas na própria unit compilada, incrementando o tamanho da mesma e também utilizando mais memória em programas compilados que usem a mesma, mas não chega a afetar o desempenho da aplicação.As opções "Include debug info" e "Generate Map file", em Project\Options\Linker, produzem informações completas de depuração somente quando o módulo está com a diretriz {$D+} setada. A opção Use Debug DCUs pode ser utilizada adicionalmente quando o problema aparentemente parece estar em partes do código da VCL, mas não chega a ser o caso deste post.


Falando um pouco mais sobre Map Files, um map file detalhado contém, entre outras coisas, uma lista dos endereços de erro e suas respectivas linhas no código fonte, agrupadas para cada arquivo de código fonte que faz parte da aplicação. Como exemplo, algumas linhas de um arquivo .map:
Line numbers for Findmap(FINDMAP.DPR) segment Findmap

9 0001:001F 10 0001:005B 11 0001:006B 12 0001:0083
13 0001:008E

Eu tinha encontrado um código que faz exatamente o mesmo que o Search\Erros faz, até descobrir aquele diálogo. Posto aqui o código pra quem se interessar.
unit Unitfind;
{$I-}
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Buttons;

type
TForm1 = class(TForm)
OpenDialog1: TOpenDialog;
MapFile: TLabel;
FileEdit: TEdit;
ErrorAddress: TLabel;
ErrorEdit: TEdit;
SourceFile: TLabel;
SourceLine: TLabel;
BitBtn1: TBitBtn;
procedure FindSourceFileAndLineNumber(Sender: TObject);
procedure FormCreate(Sender: TObject);
end;

var
Form1: TForm1;

implementation
{$R *.DFM}

procedure TForm1.FormCreate(Sender: TObject);
begin
if OpenDialog1.Execute then FileEdit.Text := OpenDialog1.FileName
end;

procedure TForm1.FindSourceFileAndLineNumber(Sender: TObject);
var f: System.Text;
LastFileName,FileName: String;
PastHeader: Boolean absolute FileName;
Str: String;
Len: Byte absolute Str;
Seg: String[6];
Off: String[4];
MinV,MaxV,NewVal,OffValue: Word;
MinL,MaxL,Line,i,error: Integer;
begin
Str := ErrorEdit.Text;
Seg := ' '+Copy(Str,1,5);
Off := Copy(Str,6,4);
MinV := 0;
MaxV := $FFFF;
MinL := 0;
MaxL := 32767;
NewVal := 0;
Val('$'+Off,OffValue,error);
Line := 0;
FileName := '';
LastFileName := 'Error: no match found!';
SourceLine.Caption := '';
SourceFile.Caption := 'Searching...';
Cursor := crHourGlass;
Application.ProcessMessages;
System.Assign(f,FileEdit.Text);
System.Reset(f);
if (IOResult = 0) then while (Line = 0) and not eof(f) do
begin
readln(f,Str);
if Pos('Line numbers for ',Str) = 1 then
begin
FileName := Copy(Str,Pos('(',Str)+1,Len);
FileName[0] := Chr(Pos(')',FileName)-1);
SourceFile.Caption := FileName;
Application.ProcessMessages;
Line := MinL { stop already? }
end
else if PastHeader then
repeat
i := Pos(Seg,Str);
if i > 0 then { next segment hit }
begin
repeat Dec(i) until Str[i] = ' ';
Inc(i);
Line := 0;
repeat
Line := 10 * Line + Ord(Str[i]) - 48;
Inc(i)
until Str[i] = ' ';
Delete(Str,1,i+5);

if Pos(Off,Str) = 1 then
begin
SourceLine.Caption := Format('Found at line %d',[Line]);
LastFileName := FileName
end
else
begin
Val('$'+Copy(Str,1,4),NewVal,error);
if error = 0 then
begin
if (NewVal > MinV) and (NewVal <= OffValue) then begin LastFileName := FileName; MinV := NewVal; MinL := Line; SourceLine.Caption := Format('Somewhere between %d and %d',[MinL,MaxL]) end else if (NewVal < MaxV) and (NewVal >= OffValue) then
begin
LastFileName := FileName;
MaxV := NewVal;
MaxL := Line;
SourceLine.Caption :=
Format('Somewhere between %d and %d',[MinL,MaxL])
end
end;
Line := 0
end
end
until (i = 0) or (Line <> 0) { found }
end;
SourceFile.Caption := LastFileName;
Cursor := crDefault;
system.close(f);
if IOResult <> 0 then { skip }
end;

end.

Continuando com a explicação, quando uma violação de acesso em tempo de execução acontece, o usuário recebe um erro parecido com este:


Se sua aplicação foi compilada com informações de depuração, você pode localizar a linha do código correspondente ao código compilado que gerou a violação de acesso.

Objeto Inexistente

Uma das causas mais comuns de erros de violação de acesso no Delphi é o uso de um objeto que ainda não foi criado.Se o segundo endereço for FFFFFFF (ou 0000000), com certeza você está tentando acessar um objeto que ainda não foi criado. Por exemplo, chamar um método em um form que não está setado como auto-create e não está instanciado via código.
procedure TfrMain.OnCreate(Sender: TObject);
var AVForm: TAVForm;
begin
//Isto irá gerar um AV
AVForm.Refresh;
end;

Suponha que AVForm esteja listado na lista "Avaliable Forms" na janela Project\Options - forms que necessitam ser criados e destruídos manualmente. No código acima, a chamada ao método Refresh do form AVForm irá causar a violação de acesso.

Se a opção "Stop on Delphi Exceptions" em Debbuger Options\Language Exceptions estiver setada então a mensagem a seguir irá aparecer:


A mensagem mostra que uma exceção EAccessViolation foi lançada. A exceção EAccessViolation é a classe de exceções para erros ligados a acessos inválidos à memória. Esta mensagem aparece quando você está desenvolvendo sua aplicação. a próxima mensagem será vista pelo usuário e, logo em seguida, a aplicação morrerá:


Access violation at address 0043F193
in module 'Project1.exe'
Read of address 000000.


O primeiro número (0043F193) é o endereço do erro de runtime no código compilado (Project1.exe) onde a violação de acesso ocorreu. Na IDE, escolha o menu "Search\Find Error...", entre com o endereço do erro no diálogo e clique em Ok. O Delphi irá recompilar o projeto e mostrará a linha do código fonte onde o erro ocorreu, que no nosso caso, foi em AVForm.Refresh.

Dando prosseguimento ao post, segue uma lista das causas de violação de acesso mais comuns no desenvolvimento em Delphi. Esta lista não é definitiva e, em nenhum momento, tem como objetivo tentar cobrir todas as situações que levam a erros de violação de acesso. Na vida real, estes erros são mais obscuros e de maior dificuldade de detecção.

"O Fantasma da Máquina" - Chamando um objeto inexistente

Como foi dito anteriormente, a grande maioria dos erros de violação de acesso em tempo de execução são relacionados a objetos que não foram criados ou que já foram destruídos. Para prevenir este tipo de erro, devemos nos certificar que todos os objetos que você está usando já foram criados pelo menos uma vez. Por exemplo, abrir uma tabela no evento OnCreate de um form quando o componente TTable está localizado em um data-module que ainda não foi criado (foi removido da lista "auto-create forms").
No exemplo abaixo, um erro de violação de acesso ocorrerá após chamar um método do objeto (b:TBitmap) que já foi destruído.
var b:TBitmap;
begin
b:=TBitmap.Create;
try
//faz alguma operação com b
finally
b.free;
end;
//Esta linha irá causar uma violação de acesso - o objeto b não existe mais
b.Canvas.TextOut(0,0,'this is an Access Violation');
end;

"Ahn ?? Como ??" - Parâmetros Inválidos em APIs

Se você tenta passar um parâmetro inválido para alguma API do sistema (na verdade, qualquer método externo à aplicação), um erro de violação de acesso ocorrerá. A melhor maneira de resolver este problema é consultar a documentação da API do sistema ou dos módulos utilizados e fornecer os tipos esperados pelo método. Por exemplo, tenha sempre o cuidado de não passar um ponteiro inválido em um parâmetro do tipo buffer.

"I believe I can fly" - Deixe o Delphi finalizar seus métodos

Quando um objeto pertence a outro, deixe que o segundo finalize o primeiro. Por exemplo, todos os forms (criados automaticamente), por padrão, são pertencentes ao objeto Application (Owning). Então, quando a aplicação terminar, ela irá finalizar o objeto Application que, em seguida, irá finalizar todos os forms. Por exemplo, se você tem dois forms (Form1/Unit1 e Form2/Unit2), ambos criados automaticamente pela aplicação, o próximo código irá gerar uma violação de acesso:

unit Unit1;
...
uses unit2;
...
procedure TForm1.Call_Form2
begin
Form2.ShowModal;
Form2.Free;
Form2.ShowModal; //bum!!
end;

"Die another Day" - Matando a Exceção

NUNCA destrua o objeto temporário de exceção (E). Tratar a exceção automaticamente destróis o objeto E. Se você destrói o objeto, a aplicação tentará destruir o mesmo e, novamente, gerará uma violação de acesso.

Zero:=0;
try
dummy:= 10 / Zero;
except
on E: EZeroDivide do
MessageDlg('Can not divide by zero!', mtError, [mbOK], 0);
E.free. // causes an access violation
end;

"I see Dead People" - Acessando posições de uma string vazia

Uma string vazia não contém nenhum dado válido. No entanto, tentar acessar um determinado membro da string vazi é como tentar acessar uma variável Nil e o resultado será, impreterivelmente, uma violação de acesso.

var s: string;
begin
s:='';
s[1]:='a'; //AV
end;

"Ei, isto é meu seu imbecil" - Referência de Ponteiros

Quando trabalhando com ponteiros diretamente, você precisa tomar o cuidado de sempre trabalhar com a informação armazenada na memória e não com o indicador da mesma. Se você, de alguma forma, alterar o "referencial" do ponteiro, ele possivelmente irá corromper outros locais da memória.

procedure TForm1.Button1Click(Sender: TObject);
var
p1 : pointer;
p2 : pointer;
begin
GetMem(p1, 128);
GetMem(p2, 128);

{Esta linha poderá causar uma violação}
Move(p1, p2, 128);

{esta está correta}
Move(p1^, p2^, 128);
FreeMem(p1, 128);
FreeMem(p2, 128);
end;

Bem, isto é tudo no momento. Espero que com estas dicas tanto programadores quanto usuários possam aprender um pouco mais sobre como as aplicações "caem", mas agora também sabem como elas podem se levantar.

Até breve! Que os bons ventos os guiem e as sendas os levem ao seu destino!!

P.S.: Nossa, que frase horrível que eu inventei. Depois dessa, vou parar de tentar ser um bardo ahuaahuahauha

Fonte: delphi.about.com