Assembly é uma linguagem de baixo nível, ou seja, é uma linguagem próxima daquela que as máquinas "entendem". Para utilizá-la é necessário conhecer não só a própria linguagem como também as características de funcionamento da máquina. Isto é coisa de doido? Nem tanto. Vou fazer algumas comparações e depois você pode decidir se vale a pena assemblar.
Arquivos executáveis de alta performance Se você tem uma rotina que acaba fritando a CPU de tanto processamento, então pense no Assembly. Os executáveis criados em Assembly apresentam duas grandes vantagens: velocidade de execução e tamanho reduzido. Além disto, como a linguagem não tem firulas e vai direto ao ponto (ou aos registradores registradores ), a execução exige muito menos recursos porque qualquer assembler supera a capacidade dos melhores compiladores de linguagens de alto nível. O software de performance crítica é um alvo natural para os programas em assembly puro.
Bibliotecas de link dinâmico (DLLs) O MASM (da micro$oft), NASM (da equipe liderada por Simon Tatham e Julian Hall), o TASM da Borland, o A386 de Eric Isaacson ou o GoAsm de Jeremy Gordon, enfim, praticamente todos os assemblers mais conhecidos são capazes de construir bibliotecas de link dinâmico de altíssima performance que podem ser utilizadas pelos próprios programas assembladores, pelo Visual C/C++, Delphi, Visual Basic, e outras linguagens capazes de chamar uma DLL DLL.. Usar um assembler significa obter arquivos de tamanho mínimo e alto rendimento que podem ser interfaceados com qualquer linguagem apta a chamar uma DLL. Algoritmos que, por exigirem processamento pesado, não podem ser implementados em linguagens de alto nível, tornam-se viáveis quando escritos em Assembly.
Módulos de biblioteca para programas Visual C/C++ O MASM produz um formato de módulo objeto idêntico aos compiladores do Visual C/C++. Isto permite construir módulos ou bibliotecas em MASM e linká-los diretamente a programas C/C++. Com isto, programadores C/C++ podem manipular áreas de código críticas de forma muito eficiente, ou seja, podem otimizar a manipulação de gráficos, dados de alta velocidade, criptografia, compressão de dados e qualquer outra forma de manipulação manipulação de informação que exija muito processamento.
Software gratuito Assembladores são softwares gratuitos, não podem ser comprados, vendidos ou incluídos em qualquer tipo de software comercial. Já que o MASM é uma das poucas coisas que a Microsoft disponibiliza sem cobrar um caminhão de US$... este é mais um motivo para começar a assemblar.
Para mim, estes quatro argumentos são mais do que suficientes. Aliás, sempre que posso e quando o peso do processamento é muito grande, opto pelo Assembly. É claro que não vou criar um aplicativo inteiro nesta linguagem, mesmo porque é para ganhar tempo na produção que existem os RAD da vida, mas enxertar código Assembly no Delphi ou no C++ são dois palitos. E o resultado é altamente compensador! Na série de tutoriais desta seção vou tratar apenas de Assembly puro (nada de mistura com outras linguagens) porque se eu misturar assuntos o resultado acaba sendo uma salada. Uma vez dominado o arroz com feijão da linguagem, criar estas entidades híbridas não é nenhum bicho de sete cabeças. E aí, está afim de me acompanhar? Então vamos lá... siga a vovó.
Links interessantes Iczelion's Win32 Assembly Homepage: Homepage : Um dos melhores sites que encontrei (um clássico da Internet) é o do Iczelion. O menino curte Assembly e, até o momento, já produziu cerca de 60 ótimos tutoriais em inglês baseados no MASM. Você pode fazer o download de tutoriais, software e exemplos de código fonte. O site está meio velhinho e não foi mais atualizado, mas o material é de primeira! hutch's home page: page: Steve Hutchesson, Hutchesson, de Sidnei (Austrália), é um nome que você vai encontrar com frequência no mundo assembly. Disponibiliza o pacote do MASM32, tools, snippets de códigos e é o autor de diversos softwares (freeware e código aberto) que são a salvação dos iniciantes e uma mão na roda para os experts.
Índice do Artigo Por onde começar Comentários Todas as páginas
Assemble vem do Inglês e significa construir, juntar partes; é daí que vem o nome de uma linguagem de programação, o ASSEMBLY. Assembler significa construtor e é o nome que se dá a programas que "juntam partes", que "constroem" executáveis. É por isto que não é possível escrever um programa em assembler (como se costuma ver por aí): o programa é escrito em Assembly e depois pode ser transformado num executável por um assembler Por onde começar é aquela parte inicial, chata pra caramba, mas que não adianta
ignorar porque vai fazer falta mais tarde. Neste texto você vai ter a oportunidade de conhecer um pouco do MASM e da estrutura de um programa.
Para se comunicar com um assembler são utilizadas algumas convenções que o programa exige para poder executar as tarefas desejadas. Cada assembler tem suas próprias convenções. Nestes tutoriais vamos utilizar o MASM32 como assembler, mas você pode usar qualquer outro da sua escolha. As diferenças são pequenas e é fácil adaptar o código.
O assembler Antes de mais nada, faça o download do macroassembler da Microsoft, o MASM 32 versão 9, na seção de downloads da Aldeia. Procure na categoria Informática|Compiladores/Decompiladores. Descompacte o arquivo zipado num diretório (quem "fala" Assembly não usa pastas, usa diretórios!) da sua escolha e clique em /seuDiretório/QEDITOR.EXE. Esta é a interface de edição de programação - não é cheia de nove horas porque programadores assembly estão acostumados a escovar bits e a fazer a maior parte do trabalho na unha sem muita frescura. Muito bem. O editor está à disposição e aí vem a pergunta: o que fazer agora? tô afim de começar! Hehehe... lembra da parte inicial, chata pra caramba? Pois é esta aqui. Tenha um pouco de paciência e, por enquanto, feche o editor e leia o resto do texto. Depois a gente conversa. A sequência do trabalho
Quando se escreve um programa, na realidade se escreve um texto que contém um roteiro que precisa ser transformado num programa. Esta transformação é feita em duas etapas. Primeiro, o texto precisa ser transformado num novo roteiro que contenha instruções para a CPU. Esta primeira tradução produz os assim chamados arquivos objeto. O programa que produz arquivos objeto é chamado de assembler ou compilador. Os arquivos objeto, por sua vez, servem de fonte para outro tipo de programa: o linker. Os linkers adicionam referências de endereços aos arquivos objeto e os transformam em arquivos executáveis, a versão final do seu programa. Resumindo as etapas, tem-se o seguinte: Arquivo Texto (.ASM) Executável (.EXE) Editor de Texto Linker
--->
Arquivo Objeto (.OBJ)
--->
Arquivo
Assembler (compilador)
O MASM é um aplicativo que pode coordenar o seu trabalho: possui um editor de texto, um compilador e um linker. Através da janela do MASM é possível gerenciar todo o processo de produção de um programa. Você pode escrever seu texto (ou script) em qualquer editor de texto, até com o bloco de notas do Windows. Como usaremos o MASM, podemos usar o editor de texto do próprio. Se escrevermos uma receita de bolo no editor de texto e pedirmos para o MASM (compilador+linker) transformá-lo num programa... hmmmm... o MASM vai ficar perdidinho da silva. É preciso criar um texto que o MASM entenda e, o que é mais importante, obedecendo uma determinada estrutura.
A estrutura de um programa
A primeira informação que o MASM precisa para poder trabalhar é o tipo de CPU para a qual o programa se destina. Isto é necessário para que o compilador possa escolher o conjunto de instruções compatíveis com a CPU. Como pretendemos produzir programas de 32 bits, indicamos o conjunto de instruções como sendo do tipo 80386 em diante (80386, 80486, Pentium, etc). Trabalhar com o conjunto de instruções do 386 costuma ser mais do que suficiente. Como é que passamos essa informação para o compilador? Usando uma diretiva apropriada: .386 ---> para processadores do tipo 80386 .486 ---> para processadores do tipo 80486
Puxa vida, por enquanto, nada de programa. Ainda precisamos indicar qual é o modelo de memória que deve ser usado. Um executável é carregado na memória de acordo com o modelo de memória definido durante a compilação. Na época dos computadores de 16 bits, o programa era carregado na memória em segmentos de tamanho predefinido. Era uma complicação danada gerenciar a execução do programa: para cada endereço era necessário indicar o segmento correspondente e saltar de um segmento para outro parecia soluço e "comia" um monte de processamento. Com o advento dos 32 bits, os executáveis passaram a ser carregados na memória em endereços contíguos. Imagine um segmento único de memória contendo o executável, uma tripa enorme com a sequência de instruções. Este modelo de memória foi denominado FLAT, ou seja, modelo plano ou contínuo. Então, lá vai mais uma diretiva: .MODEL FLAT
Qualquer programa, com toda certeza (tem tanta certeza assim?) vai realizar algum trabalho, ou seja, vai ter funções. As funções geralmente precisam de dados para executar uma tarefa. Estes dados podem ser gerados dentro da própria função ou serem enviados para elas. Dados enviados a uma função são chamados de parâmetros. A forma de mandar estes parâmetros também precisa ser definida previamente: se houver mais de um parâmetro, podemos enviá-los de frente para trás ou de trás para frente, ou seja, da esquerda para a direita ou da direita para a esquerda. Veja um exemplo: suponha que uma função espere receber dois parâmetros (param1 e param2). Podemos enviá-los na sequência param1, param2 ou na sequência param2, param1. A primeira convenção de passagem de parâmetros é conhecida como PASCAL e a segunda como convenção C . Os parâmetros recebidos são guardados temporariamente num registrador da CPU chamado de pilha (stack). Imagine a pilha como uma pilha de caixas de sabão em pó no supermercado. À medida que você colocar novas caixas na pilha, a pilha vai crescendo; à medida que tira, a pilha vai encolhendo; se você tirar uma caixa do meio da pilha, as caixas vão cair. Como o conteúdo da pilha é alterado quando uma função é chamada ou quando a própria função alterá-lo, é preciso fazer um ajuste de pilha em cada retorno de função... senão a pilha "cai" e o programa dá pau. Na convenção de passagem de parâmetros do tipo Pascal, a função chamada é a responsável pelo ajuste da pilha antes do retorno. Na convenção C, a rotina chamadora é a responsável. Existe
uma
terceira
convenção
de
passagem
de
parâmetros
denominada STDCALL (abreviação de STanDard CALL - chamada padrão). Usando esta convenção, os parâmetros são enviados da direita para a esquerda (como na convenção C) mas a função chamada é a responsável pelo ajuste da pilha. A STDCALL é um híbrido das convenções Pascal e C. Os sistemas win32 utilizam exclusivamente a convenção de passagem de parâmetros STDCALL. Podemos e devemos completar a diretiva acima com: .MODEL FLAT, STDCALL
Na maioria das vezes, um programa precisa de dados inicializados (com valores definidos) para poder começar a funcionar. São coisas do tipo nome do aplicativo, título da janela principal, etc. Para indicar ao MASM que vamos listar nomes de variáveis e seus respectivos valores, usamos a diretiva .DATA. Tudo que o compilador encontrar nas linhas subsequentes, até encontrar outra diretiva, ele vai considerar como variáveis (dados) inicializados. Também podemos preparar variáveis não inicializadas , ou seja, fazemos com que o assembler ponha determinados nomes de variáveis na lista de variáveis, mas sem valores definidos. Estes dados não inicializados podem ser usados posteriormente pelo código. A diretiva .DATA? faz a indicação. Variáveis, como o próprio nome indica, podem ter seus valores alterados. Num determinado projeto pode ser que sejam necessários dados de valores fixos , as assim chamadas constantes. Neste caso, utilizamos a diretiva .CONST. Todos estes dados, indicados antes do programa propriamente dito, podem ser usados em qualquer ponto do código. Tanto faz se estamos no módulo principal ou em alguma subrotina (função) - estes dados estão sempre disponíveis e acessíveis. São os chamados dados globais (variáveis e constantes). Enquanto o programa estiver sendo executado, estes dados ficam preservados. Só são destruídos quando o programa termina. Ufa! Finalmente chegamos no miolo do programa. É onde deve ficar o código que indica como nosso programa deve se comportar e o que deve realizar. A diretiva usada não poderia ser outra: .CODE indica o início do nosso código. Só que esta é a última diretiva da estrutura e o assembler não tem como saber onde ela termina. Estabelecemos então o limite com um rótulo seguido por dois pontos. O dá um nome à área de código e indicamos o fim da área com um "end ". Você pode escolher o nome que quiser, por exemplo: .CODE ---> Início do código inicio: ---> Rótulo indicando o início da área de código ...
(seu código)
end inicio
---> Fim da área de código
Resumindo, a estrutura que o MASM entende e aceita para assemblar e linkar é a seguinte: .386
.MODEL FLAT, STDCALL .DATA ... (aqui vão os dados inicializados) .DATA? ... (aqui vão os dados não inicializados) .CONST ... (aqui ficam as constantes) .CODE inicio: ... (aqui está todo o código do programa) end inicio
Palpites da vó
Por enquanto, é só. Depois que você estiver familiarizado com o Assembly, vai usar esta estrutura de diretivas automaticamente, sem problema algum - mas sempre é bom revisar o que realmente significam e como devem ser utilizadas. Para você que está começando, então isto tudo é novidade. Dê mais uma lida no texto e fixe bem os conceitos para evitar futuras dores de cabeça com programas "mal comportados". Veja quais são os os arquivos envolvidos na rotina de criação de um programa. Para criar um programa escrito em Assembly e que rode no ambiente Windows e *nix, várias etapas precisam ser cumpridas. Inicialmente escreve-se o chamado código fonte ou script do programa e, se necessário, um arquivo de recursos. Estes arquivos, em texto ASCII puro, serão transformados em arquivos objeto os quais, por sua vez, serão utilizados para compor o executável. Observe que são três etapas distintas: elaboração do código fonte, compilação e linkedição.
As etapas Elaboração Compilação Linkedição Arquivos fonte Arquivos objeto Executável .asm .obj .exe ou .dll .rc .res Os arquivos de recursos (.rc) só são necessários quando se quer utilizar recursos adicionais, como menus e bitmaps. Isto significa que podem ou não estar presentes no seu projeto. Vamos explorar cada uma das fases.
O código fonte Os arquivos que contêm o código fonte deve ser gravados em texto ASCII puro, ou seja, não devem conter caracteres especiais de formatação. Podem ser produzidos em
qualquer editor de texto que ofereça a possibilidade de salvar arquivos neste formato (geralmente chamado simplesmente de formato texto). O bloco de notas do Windows é um bom editor para este trabalho, mas você pode usar o editor que mais lhe convier, inclusive o editor oficial da Aldeia, o SilicioPad, que nada mais é do que o bloco de notas modificado para mostrar o número das linha no rodapé da janela (como fazer esta mandrakaria será tema de um tutorial avançado). Se quiser experimentar, faça o download do SilicioPad em Downloads/Informática/Editores. É hábito dos programadores indicar a linguagem usada na extensão do arquivo fonte. Por exemplo, .asm para código fonte em Assembly, .c para a linguagem C, .cpp para a C++ ou .pas para Pascal (Delphi). O programa assembler (ou compilador) aceita qualquer extensão, contanto que o código fonte seja o que ele espera. Um arquivo .asm contém as instruções para o processador na forma de palavras e números. Estas instruções serão executadas pelo processador quando o programa rodar. Acontece que o processador não entende o código fonte: ele precisa ser transformado em "linguagem de máquina" e estar "arrumado" de acordo com um padrão que o sistema operacional consiga identificar. Esta transformação é feita em duas etapas: o compilador prepara o código fonte guardando o código fornecido no formato COFF (Common Object File Format) num arquivo objeto (.obj) e este, por sua vez, é transformado pelo linkeditor num arquivo executável (.exe) no formato PE (Portable Executable).
O arquivo objeto Um arquivo objeto é criado pelo compilador ou assembler a partir de um arquivo .asm. O compilador pega as instruções do arquivo .asm, que estão em palavras e números, e as transforma no formato objeto COFF, que é o formato que o linker espera. O compilador concatena todo o código e os dados de instrução presentes no código fonte e os coloca em seções de código e de dados no arquivo .obj. A seção de código contém as instruções, os chamados códigos operacionais (opcodes) que o processador executa quando o programa é rodado. A seção de dados contém informações que serão mantidas na memória enquanto o programa estiver sendo executado. Não é possível rodar um arquivo .obj como um programa, por que não tem a forma final de um executável. O formato de um executável esperado pelo Windows é o formato PE. Algumas vezes o arquivo .obj é chamado de arquivo "binário" ou simplesmente "bin", o que se justifica porque ele não contém mais os mnemônicos, apenas números.
O código fonte dos recursos Como todo código fonte, este também é um arquivo texto produzido com um editor de texto. Deve conter palavras e números que o sistema operacional usa para montar os recursos do programa que será executado os quais, geralmente, são menus, diálogos e tabelas de strings. Também serve de fonte de referência para outros arquivos como ícones, bitmaps e cursores. Maiores detalhes em como criar um arquivo de recursos você encontra nos tutoriais.
O arquivo objeto dos recursos O arquivo de recursos compilado é como se fosse um "arquivo objeto" dos recursos. Normalmente não se usa este termo, mas a comparação é perfeitamente válida. Este arquivo é produzido por um compilador de recursos, o qual formata as instruções contidas num arquivo .rc (palavras e números), convertendo-o num arquivo .res pronto para ser inserido na seção de recursos de um executável final por um linker.
O executável O arquivo executável é o arquivo final que pode ser executado pelo sistema operacional. Geralmente está no formato PE, reconhecido pelo Unix, Linux, Windows, etc. O programa é produzido por um linker, o qual usa um ou mais arquivos .obj e .res e os combina num executável final. O formato PE também exige que o executável tenha um cabeçalho com informações a respeito do arquivo .exe. O linker fornece estas informações.
Arquivos DLL Este tipo de arquivo contém funções e dados que outros executáveis podem utilizar quando estiverem rodando. São úteis quando contêm funções que serão chamadas repetidas vezes por vários executáveis. Ao invés de repetir estas funções em cada um dos executáveis, estes utilizam uma fonte comum: as DLLs. O acrônimo DLL originase de Dinamic Link Library - Biblioteca de Vínculo Dinâmico.
Se você já leu os tutoriais "Porque Assembly" e "Por onde começar", já é um bom começo. A idéia é esta mesmo: ir seguindo os tutoriais na sequência em que são listados. Posso imaginar que os iniciantes estejam doidinhos para por a mão na massa, mas sinto dizer, ainda falta mais uma coisinha: não adianta querer começar a programar sem saber o que são códigos operacionais e mnemônicos, o que este tutorial curtinho vai explicar.
A "linguagem" binária Em nível de hardware só existe um "idioma": o binário. Isto significa que o processador (e todos os outros componentes de um computador) só entendem "binarês", ou seja, bits ligados ou desligados. Digamos que eu queira enviar o valor 1.453.785.342 para um registrador da CPU. Para que o computador "entenda" o que estou querendo dizer, este valor precisa chegar no formato binário. Considerando que bits ligados sejam iguais a 1 e bits desligados sejam
iguais a 0, a seguinte "tripa" de bits precisa ser enviada: 1010110101001110000000011111110
<-- 1.453.785.342 em binário
Tá na cara que não vai dar para programar desta forma, mesmo por que ninguém é maluco o suficiente para querer transformar opcodes e valores nestas "tripas" ininteligíveis.
Valores hexadecimais Ainda em nível de hardware, num processador Intel ou compatível, as instruções estão embutidas nos circuitos e são denominadas de "opcodes" - códigos operacionais. Cada código operacional é identificado por um valor binário. Para realizar uma transferência para o registrador eax, por exemplo, o opcode é 10100001. Apesar da informação poder ser escrita em nível de bit, o tamanho mínimo dos dados e códigos normalmente utilizado é um BYTE (8 bits). Em nível de BYTE, o código pode ser escrito na notação hexadecimal, porém este método de escrever ainda é muito complexo e exige o conhecimento de um grande número de opcodes. Aliás, o opcode 10100001 para transferir um valor para eax é A1 em hexadecimal. Além disto, existe uma dificuldade adicional: os processadores Intel têm a peculiaridade de armazenar os dados em ordem inversa. Por exemplo, se quisermos copiar o valor 56 A7 00 FE para o registrador eax, precisamos enviar este valor como FE 00 A7 56. Só para você saber, esta é a notação hexadecimal do mesmo valor decimal e binário citado acima. Se você não tem idéia do que vem a ser a notação hexadecimal, dê uma chegada na Escolinha da Aldeia e conheça os principais sistemas de notação. Entre eles, você vai conhecer os sistemas binário e hexadecimal
Assim não dava para ficar Para facilitar o trabalho de escrever código de baixo nível, há muitos anos atrás foi desenvolvido um sistema onde grupos de códigos operacionais semelhantes receberam nomes que lembram suas funções e que os tornaram muito mais práticos de serem usados. Estes nomes são denominados MNEMÔNICOS. Ficando nos exemplos citados, o opcode 10100001 (em binário), ou A1 (em hexadecimal), foi trocado pelo mnemônico MOV (de mover, transferir). Este é o sistema utilizado nos assemblers de 32 bits modernos. Um mnemônico é um nome reservado de uma família de códigos operacionais que realizam tarefas semelhantes no processador. Os códigos operacionais atuais diferem quanto ao tamanho e ao tipo de operandos que são utilizados. Por exemplo, se quisermos usar o mnemônico MOV, podemos escrever mov eax, var1
; move a variável var1 para o registrador eax ; o valor hexadecimal deste MOV é A1
mov var1, eax
; move o valor de eax para a variável var1 ; o valor hexadecimal deste MOV é A3
E viva os mnemônicos! No MASM32, assim como em outros macroassemblers, usamos apenas mnemônicos... graças a Deus! Conversamos usando mnemônicos como MOV, MUL (multiplicar) ou JP (jump=saltar) e deixamos a tarefa da tradução para o "binarês" para o assembler
Assembly é uma linguagem de programação e uma linguagem de programação serve para fazer programas. Os programas são escritos em forma de texto. Usando um editor de texto criamos o chamado código fonte. Este código fonte é transformado pelo compilador e pelo linker num programa executável. Muitas vezes ouvimos "linguagem assembler". É um erro muito difundido. Na realidade, Assembly é o nome da linguagem e assembler é um programa capaz de compilar código fonte em arquivos objeto. A linguagem Assembly é considerada de baixo nível. Isto não significa que seja menos importante ou eficiente que uma linguagem chamada de alto nível - são apenas modos diferentes de se programar e níveis diferentes de atuação. O que posso dizer é que, com uma linguagem de baixo nível como a Assembly, você pilota diretamente a CPU do seu computador - nada de intermediários. Uma das características da Assembly é que cada linha do código fonte possui apenas uma instrução para o processador (CPU). Por exemplo, MOV EAX,EDX irá MOVer o conteúdo do registrador EDX para o registrador EAX. Neste caso, a instrução "MOV" é chamada de mnemônico. Os mnemônicos são os "apelidos" das instruções, mais fáceis de guardar na memória do que seu valor hexadecimal ou seu valor binário exigido pelo processador. De mnemônico em mnemônico podemos escrever nosso código fonte e fazer com que o processador faça exatamente o que queremos, sem firulas ou perda de tempo. O resultado é um programa enxuto, rápido e altamente eficiente. Tome coragem! Experimente programar em Assembly!
Os componentes da linguagem Assembly Os componentes da linguagem Assembly são basicamente as instruções para o processador. Ignorando as instruções que não podem ser utilizadas pelo sistema operacional Windows, assembly condicional, macros, ponto flutuante, MMX e instruções de 64 bits, os componentes da linguagem Assembly podem ser divididos nas seguintes categorias:
Instruções de registradores Instruções de pilha
Instruções de execução Instruções de memória Instruções de flag Declarações de memória Diretivas para o assembler Comentários Instruções para o sistema operacional (Windows)
Instruções de registradores Estas instruções transferem dados ou realizam cálculos utilizando os registradores de 32 bits da CPU. Existem seis registradores de uso geral chamados de EAX, EBX, ECX, EDX, ESI e EDI. Exemplos deste tipo de instrução são: MOV ESI,EBX ADD EAX,EDI BT ECX,0 CMP EDX,450 DIV ECX MUL ECX SHL EDX,4 por 16) TEST EAX,8
;move o conteúdo do registrador EBX para o registrador ESI ;soma o conteúdo do registrador EDI com o do registrador EAX ;testa o bit 0 do registrador ECX ;compara o conteúdo de EDX com 450 ;divide EDX:EAX (inteiro longo) por ECX ;multiplica EAX por ECX e põe o resultado em EDX:EAX ;desloca os bits de EDX para a esquerda em 4 bits (multiplica ;testa o bit 3 do registrador EAX
Instruções de pilha A pilha é uma área de memória reservada pelo sistema operacional como área de arquivamento temporário para cada programa que estiver rodando. São exemplos deste tipo de instrução: PUSH EAX ;põe o conteúdo do registrador EAX na pilha ;retira o último valor colocado na pilha e põe em EDX POP EDX ;põe o valor hexadecimal 1000 na pilha PUSH 1000h MOV EBP,
Extended Stack Pointer
Ponteiro de Pilha Estendido - um registrador que guarda o endereço do topo da pilha.
', CAPTION, 'ESP',BELOW,RIGHT, WIDTH, 300, FGCOLOR, '#CCCCFF', BGCOLOR, '#333399', TEXTCOLOR, '#000000', CAPCOLOR, '#FFFFFF', OFFSETX, 10, OFFSETY, 10);" onmouseout="return nd();" > ESP ;move o valor do ponteiro da pilha para o registrador EBP SUB ESP,30h ;move o ponteiro da pilha para abrir uma área de armazenamento para dados locais MOV D[EBP-20h],500h ;insere o valor 500 hexa na área de dados locais
Instruções de execução
Estas instruções desviam o processador para que execute código a partir de um ponto que não seja a próxima linha de execução. São exemplos: CALL MAKEWINDOW CALL EAX depois retorna RET JZ 4 marcador 4: JC >.fim JMP MAKEWINDOW LOOP 2
;executa o código do procedimento e depois retorna ;executa o código a partir do endereço presente em EAX e ;termina este procedimento retornando ao chamador ;se o resultado for zero, continua a execução a partir do ;se a flag estiver ativa, continua a execução a partir de .fim ;continua a execução a partir do procedimento nominado ;decrementa ECX e salta para o marcador 2: a não ser que ECX=0
Instruções de memória Estas instruções lêem ou escrevem em áreas de memória que não sejam da pilha. Normalmente estas áreas estão na seção de dados do próprio executável ou podem ser alocadas pelo sistema operacional em tempo de execução. São exemplos: ADD EAX,[ESI] ;adiciona a EAX o conteúdo de memória cujo ponteiro de endereço está no registrador ESI MOV EAX,[MEUSDADOS] ;move para EAX o conteúdo de memória cujo marcador é MEUSDADOS SUB D[MEUSDADOS+64],10h ;subtrai 10h do dword em MEUSDADOS mais 64 bytes CMP B[MEUSDADOS+EDX*4],2 ;compara um byte com 2 numa parte do array MEUSDADOS ;carrega o byte na memória apontada por ESI em al LODSB STOSD ;carrega o conteúdo de EAX na memória apontada por EDI
Instruções de flag As principais flags usadas são a Z (flag zero), C (flag carry), S (flag de sinal) e D (flag de direção). A maioria das instruções alteram as flags automaticamente para mostrarem o resultado da instrução. Existem determinadas instruções que podem ser usadas para alterar o valor das flags manualmente: STC CLC STD CLD
;ativa ;limpa ;ativa ;limpa
a a a a
flag flag flag flag
de de de de
carry carry direção para LODS, STOS, CMPS, SCAS, MOVS direção
Declarações de memória O sistema operacional reserva memória para o executável quando ele é executado. Declarações são feitas para reservar memória na seção de dados ou na seção de constantes se os dados devem ser inicializados, isto é, devem receber um valor. Se
forem dados não inicializados, a área de dados pode ficar reservada na seção de dados não inicializados. Isto não toma espaço algum no arquivo executável, por que um espaço de memória é alocado para este tipo de dado quando o executável é iniciado pela primeira vez. Seguem exemplos de como a memória é declarada, o que pode variar de acordo com o assembler utilizado: DB 4 ;declara um byte e lhe atribui o valor inicial 4 MEUDADO DB 4 ;um byte de valor inicial 4 com o marcador MEUDADO MYSTRUCT DD 16 DUP 0 ;16 dwords, todos com valor zero, chamados MYSTRUCT ;1024 bytes chamados BUFFER como dados não definidos BUFFER DB 1024 DUP ?
Diretivas para o assembler São instruções que orientam onde o Assembler deve colocar o código fonte que as segue. O Assembler marca a seção de código como apenas para leitura e executável; as seções de dados definidos e indefinidos como leitura/escrita. Veja alguns exemplos (que podem variar de acordo com o assembler): CODE SECTION DATA SECTION CONST SECTION
;tudo o que se segue deve ser colocado numa seção ;marcada para apenas leitura e executável (código) ;tudo o que se segue deve ser colocado numa seção ;com atributos de leitura e escrita mas não de código ;tudo o que se segue deve ser colocado numa seção ;com atributo de apenas leitura
Comentários Após ponto e vírgula, o texto é ignorado até a próxima quebra de linha. Desta forma é possível associar descrições e explicações ao código fonte, as quais serão ignoradas pelo assembler.
Instruções para o sistema operacional Proporcionam ao programador o acesso a uma grande variedade de funções. No caso do sistema operacional Windows, proporcionam acesso à API. Veja os exemplos abaixo: PUSH 12h CALL GetKeyState TEST EAX,80000000h ligado) JZ >L22 PUSH 24h PUSH ESI,EDI PUSH [hWnd]
;põe valor hexa 12 na pilha para a chamada à API ;pede ao Windows para por o estado da tecla Alt em EAX ;testa se a tecla Alt está sendo pressionada (bit 31 ;não, saltar para L22 ;valor hexa 24 = ponto de interrogação, botões yes e no ;endereço do título, endereço da mensagem ;manipulador da janela proprietária
CALL MessageBoxA CMP AL,7 JNZ >L40 PUSH 0 PUSH ADDR FILE_DONE resultado PUSH ECX,EDX PUSH ESI CALL WriteFile PUSH 808h,5h PUSH EBX,EDX CALL DrawEdge PUSH 4h,3000h,ESI,0 CALL VirtualAlloc leitura/escrita PUSH 0,[hInst],0,0 PUSH 208,130,30,300 PUSH 80C80000h PUSH EAX PUSH 'LISTBOX' PUSH 0 CALL CreateWindowExA
;mostra a caixa de mensagem Windows pedindo yes/no ;checa se "no" foi clicado pelo usuário ;não, saltar para L40 ;dá endereço do arquivo FILE_DONE para receber o ;ECX = bytes que devem ser escritos, EDX=fonte de dados ;ESI = manipulador do arquivo ;escrever ECX bytes de EDX para ESI ;808 = em baixo e meio preenchido, 5 = elevado ;ebx = RECT, EDX = contexto do dispositivo ;desenhar retângulo especial com bordas na tela ;4h = fazer memória leitura/escrita, 3000h = reservar ;reservar e consignar ESI bytes de memória ;param, manipulador do módulo, menu e proprietário ;altura, largura, y, x ;estilo (POPUP+CAPTION+SYSMENU) ;EAX = endereço da string terminada em zero com o título ;por ponteiro para 'LISTBOX' na pilha ;estilo extended (nenhum) ;criar a janela listbox
...... ou, se preferir, usar INVOKE .. INVOKE CreateWindowExA, 0,'LISTBOX',EAX,80C80000h,300,30,130,208,0,0,[hInst],0 ............. INVOKE ShowWindow, [hWnd], 1
Bit e Binário, veja o que é possível fazer com um simples bit. Um bit é um elemento elétrico dentro do computador que pode estar "ligado" ou "desligado". Em termos físicos, é um semi-condutor capaz de conduzir pequenas quantidades de eletricidade quando está "ligado", coisa que não consegue quando está "desligado". Quando está "ligado" pode ser considerado como tendo o valor 1 (um). Em linguagem de computador diz-se que o bit está "setado" (do inglês - set). Quando está "desligado", pode ser considerado como tendo o valor zero. Em linguagem de computador o bit está "zerado". Bits só podem estar setados ou zerados - não possuem qualquer outro estado. Portanto, um bit só pode ter um valor binário de 0 ou 1. Dois ou mais bits podem ser associados para criar números maiores. Quando bits são associados, o bit da direita é chamado de menos significativo e, se estiver setado, representa o valor um. O próximo bit, à esquerda, é chamado de mais significativo e possui o fator dois. Quando este bit estiver setado, ele representa o valor dois. Agora imagine um número formado por dois bits. O número em binário pode ser 00 ou 01 ou 10 ou 11 e seus equivalentes no sistema decimal são 0, 1, 2 e 3.
Byte, word e dword
Bytes, words e dwords são blocos de dados básicos usados em programação. O processador irá trabalhar com o tamanho de dados adequados para executar corretamente as instruções que receber. Um byte possui 8 bits, um word possui 16 bits. Partindo desta premissa, um word possui 2 bytes (16 bits) e um duplo word (dword) possui 4 bytes ou 32 bits. Um byte pode representar valores decimais de 0 a 255. O valor decimal 255 é o total dos valores de todos os 8 bits de um byte, ou seja, decimal 128, 64, 32, 16, 8, 4, 2 e 1. Some estes valores e você vai chegar no valor 255. Isto quer dizer que, quando todos os oito bits do byte estão setados, o valor do byte é 255. Um word pode representar os valores decimais de 0 a 65.535 (o que é o mesmo que dizer 64 Kilobytes). 64 Kb é o total dos valores de todos os 16 bits setados, assim como na explicação anterior. Um dword (literalmente um "double word" ou "palavra dupla") pode representar os valores decimais de 0 a 4.294.967.295 (4 Gigabytes). 4 Gb ou 4 giga é o total dos valores de todos os 32 bits. Existem blocos de dados maiores que o processador consegue manipular - um qword (literalmente "quad word" ou "palavra quádrupla") são 64 bits de dados (4 words ou 8 bytes), um tword possui 80 bits de dados (10 bytes) e algumas instruções podem usar até 128 bits (16 bytes). Dá para perceber que estes números são bastante grandes. Agora vamos para valores menores. Um nibble é a metade de um byte, ou seja, possui quatro bits de dados. Num byte existem dois nibbles e cada um deles só consegue representar valores decimais de 0 a 128. Este valor pode ser representado como um único número hexadecimal. Tome o número hexadecimal 148C como exemplo. Ele é constituído por dois bytes. O primeiro byte contém o valor 14h e o segundo 8Ch. Neste caso, os nibbles contêm os valores 1, 4, 8 e C (em hexadecimal).
Sistemas de notação Se você quiser conhecer bit e binário em maiores detalhes, então leia Sistemas de Notação.
Os programadores representam valores na forma hexadecimal por vários motivos. Um motivo é porque é conveniente visualizar o número em forma de dados. Isto não só ajuda a lidar com números muito grandes, mas também permite saber quais bits estão "setados" e quais estão "zerados", algo muito útil quando bits individuais precisam ser testados. Outra razão é que, usando números hexadecimais, a aplicação de instruções lógicas (por exemplo OR, AND, TEST e BT) torna-se mais fácil e menos sujeita a erros. Números hexadecimais possuem base 16. A denominação hexa também é usada
(hexa=6 e deci=10 indica a base 16). Cada número hexa pode ter um valor de 0 a 9 e de A a F. Cada número hexa representa quatro bits de dados binários. Na tabela abaixo encontram-se os valores que podem ser criados com quatro bits e seus respectivos valores hexa e decimal:
Binário Hexa Decimal 0000 0 0 0001 1 1 0010 2 2 0011 3 3 0100 4 4 0101 5 5 0110 6 6 0111 7 7 1000 8 8 1001 9 9 1010 A 10 1011 B 11 1100 C 12 1101 D 13 1110 E 14 1111 F 15 Um byte é formado por 8 bits e pode ser representado por dois dígitos hexa; um word tem 16 bits e pode ser representado por quatro dígitos hexa; um dword (duplo word) tem 32 bits e pode ser representado por oito dígitos hexa. Você percebe a verdadeira vantagem de usar números hexadecimais à medida que os números vão se tornando maiores. Observe a tabela abaixo:
Binário
Hexa
Decimal
Tipo
1000 0000
80
128
byte
1000 0000 0000 0001
80 01
32 769
word
1111 1111 1111 1111
FF FF
65 535
word
1000 0000 0000 0000 0000 0000 0000 0001 80 00 00 01 2 147 483 649 dword 1111 1111 1111 1111 1111 1111 1111 1111 80 00 00 01 4 294 967 295 dword Para programar em assembly, você precisa ter o sistema hexadecimal na ponta da língua. Se quiser mais informações, leia Sistemas de Notação.
Fora do mundo dos computadores considera-se que os números são
infinitos. Sempre é possível somar mais um ou adicionar alguns zeros fazendo com que o número não pare de crescer. Para um computador os números são essencialmente diferentes por que o tamanho dos blocos de dados utilizados É FINITO. Por exemplo, um byte não consegue guardar um número maior do que 255. Se, numa instrução de byte, for adicionado 1 a 255, a soma retorna zero. Na prática, ocorrendo esta situação, a instrução faz com que a flag de carry (ou flag de transposição) seja setada para 1 para mostrar que o novo número é grande demais para o tamanho de dados que foi utilizado. Flag significa bandeira e as bandeiras são "levantadas" quando recebem o valor 1 para dar algum tipo de aviso. Fica mais fácil entender quando se observa os bits:
Flag de Carry Binário Decimal 1111 1111 255 0000 00001 1 1 0000 0000 0
Números com sinal Como o tamanho dos dados é finito, pode-se considerar os dados como tendo dois valores simultâneos, um positivo e outro negativo. Explico. Quando somamos dois números decimais, frequentemente colocamos um "vaium" na próxima coluna, assim: 11 95 + 58 ----153
A soma de dois números hexadecimais não é muito diferente. Somar 3 com F nos dá 2, com um "vai-um" para a próxima coluna: Soma decimal 3 + 15 ----18
Soma hexadecimal 1 3 + F ---12
Agora veja o que acontece quando somamos 5 com FF: Soma decimal 1 5 + 255 ----260
Soma hexadecimal 11 05 + FF ----104
Soma binária 1 1111 111 0000 0101 + 1111 1111 -----------1 0000 0100
Os "carries" (vai-um) sucessivos se movem para as posições à esquerda. Se ignorarmos o último 1, porque "estourou" o tamanho do byte, obtemos a resposta 4. Isto significa que FF (255 decimal) se comportou como -1 (5-1=4), portanto, como um número com sinal. Se considerarmos FF como um número sem sinal, o resultado é um erro de overflow porque o byte não consegue guardar o valor hexa 104. O resultado só poderia ser guardado se estivéssemos trabalhando com um bloco de dados word, de 16 bits, ou maior. Se o número é ou não negativo depende do bit mais significativo do bloco de dados (o bit mais à esquerda), que é conhecido como o "bit de sinal". Se os dados estiverem com o bit de sinal setado, então o número é um número negativo com sinal. Se o bit de sinal estiver zerado, então é um número positivo com sinal. Portanto, é fácil reconhecer um número com sinal. Se trabalharmos com words de 16 bits podemos obter valores com números sem sinal que vão de 0 a 65535 (ou de 0000 a FFFF em hexa). Se trabalharmos com números com sinal, metade dos valores serão positivos e a outra metade serão valores negativos. Observe o bit de sinal dos blocos:
Hexa
Binário
0000 0000 0000 0000 0000 Positivos
...
...
7FFF 0111 1111 1111 1111 8000 1000 0000 0000 0000 Negativos
...
...
FFFF 1111 1111 1111 1111 Em todas as formas binárias dos números positivos de 16 bits, o bit de sinal (bit 15) é sempre zero. Para todos os números negativos o bit de sinal é sempre 1. O mesmo raciocínio serve para valores armazenados em blocos de 32 bits, onde o bit de sinal é o 31. Se estiver setado, o valor é negativo; se estiver zerado, o valor é positivo. Se considerarmos apenas os inteiros positivos sem sinal o valor varia de 0 a 4.294.967.295.
Complemento de dois Os números negativos acima descritos são conhecidos como Complemento de Dois dos números positivos. Diz-se "de dois" porque a conversão é feita em dois passos. O primeiro deles é achar o complemento. O segundo é adicionar 1 ao complemento encontrado. Para obter o complemento de um número, tomamos sua forma binária e invertemos todos os bits. Por exemplo:
Decimal Hexa 240
F0
Binário 1111 0000
Complemento Hexa Decimal 0000 1111
0F
15
76
4C
0100 1100
1011 0011
B3
179
O segundo passo é adicionar 1 ao resultado do complemento: Complemento de dois de F0 0000 1111 (0F) + 1 (01) ----------0001 0000 (10)
Complemento de dois de 4C 1011 0011 (B3) + 1 (01) ----------1011 0100 (B4)
Fundiu a cabeça? Não se procupe, com o tempo a coisa fica bem mais fácil. Não se deixe intimidar por tão pouco! Você sempre pode voltar e dar uma revisada
Da mesma forma que é possível considerar o computador como sendo constituído por elementos físicos, como o teclado, o monitor e as unidades de disco, o PC pode ser descrito em termos de blocos de elementos funcionais. A parte do computador que faz a "computação", por exemplo, é chamada de microprocessador. Este é ligado a diversos componentes que formam o que conhecemos como o computador completo. Generalizando, o PC é formado por um microprocessador , memória e dispositivos diversos.
Na programação assembly é essencial conhecer os elementos funcionais do computador. Afinal de contas, se assembly é linguagem de máquina, precisamos conhecer a máquina O diagrama ao lado (clique para ampliar) mostra de forma simplificada o sistema do computador. Observe que o computador pode coordenar e executar uma grande variedade de funções devido aos seus circuitos integrados de apoio, todos eles ligados ao microprocessador por uma série de buses. Cada um destes itens funcionais, seja a memória ou um dispositivo como o teclado ou o monitor, fica ligado pelos buses de controle, de endereços e de dados.
O bus de controle, por exemplo, tem sinais que indicam quando os dados estão disponíveis para leitura. Os buses de endereços e de dados são usados para o acesso aos dispositivos e à memória. Cada item ligado ao bus de endereços pode reconhecer uma combinação exclusiva de sinais eletrônicos, conhecida como seu endereço. O microprocessador fornece os sinais e depois utiliza o bus de dados para a transferência dos dados. Quando ele quer ler dados da memória, sinaliza o local da memória desejado no bus de endereços e lê os dados vindos pelo bus de dados. A exata sincronização da sinalização de endereços e leitura de dados é dada pelo bus de controle. O microprocessador sinaliza para o circuito de controle o endereço de memória que ele quer ler e o circuito de controle de memória avisa quando os dados estiverem disponíveis. Um sistema central de sincronização, conhecido como clock do sistema, fornece um pulso regular e síncrono para o microprocessador e todos os outros componentes do computador. Este clock é gerado por um dispositivo eletrônico conhecido como gerador de clock, que é ligado a um cristal. Quando passa uma voltagem por este cristal, ele produz um sinal numa determinada frequência. A medida de tempo para o clock do sistema é a velocidade do clock, que é considerada a velocidade do computador. Um computador de 733 MHz, por exemplo, tem um sinal de clock que oscila 733 milhões de vezes por segundo. O software do computador é uma série de instruções e dados que o microprocessador processa. Cada instrução demora um número específico de pulsos do clock do sistema. Durante um ciclo de clock, o microprocessador lê a próxima localização na memória e executa a instrução encontrada. Entretanto, nem todos os itens de um PC podem funcionar usando este pulso regular. Itens que podem funcionar com pulsos são chamados de síncronos, aqueles que não podem são considerados assíncronos. Por exemplo, quando uma tecla é digitada, não é possível sincronizar a digitação com o clock do sistema porque não é possível prever quando a tecla será acionada. Os eventos assíncronos são tratados pelo controlador de interrupção. Este serve como interface entre o microprocessador e dispositivos como o teclado, um drive de disco ou o monitor. Se um sinal de interrupção foi alterado desde a última vez que o controlador de interrupção verificou, o controlador identifica o dispositivo assíncrono, o microprocessador detecta este sinal e literalmente interrompe seu processamento para atendê-lo.
A Unidade Central de Processamento (CPU - Central Processing Unit), também chamada simplesmente de Processador ou Microprocessador, é o "cérebro" do computador. Sua missão consiste em controlar e coordenar todas as operações do sistema. Extrai todas as instruções dos programas residentes na memória do computador (memória RAM), uma de cada vez, as analisa e emite as ordens necessárias para serem realizadas. Para entender como funciona um microprocessador, primeiro é necessário ter uma idéia muito clara das partes ou blocos que o compõem. Sem conhecer a arquitetura básica de um processador é praticamente impossível entender seu funcionamento. De forma geral, podemos considerar que os processadores possuem dois grandes blocos ou
unidades: a de controle, com duas unidades responsáveis pela decodificação e execução, e a unidade aritmético-lógica (ALU - aritmethic logic unit). | Decodificação UNIDADE DE CONTROLE --------------| | Execução ALU - UNIDADE ARITMÉTICO-LÓGICA
A unidade de decodificação, uma das principais unidades da unidade de controle, identifica a instrução que está para ser executada. Quando o processador lê uma instrução que está na memória, o código desta instrução é enviado para esta unidade. A unidade então interpreta este código, verifica se é válido e determina o tipo da instrução que deve ser executada (por exemplo uma soma), a trasnferência de dados para a memória e o que mais for necessário. Uma vez identificada a instrução, a unidade de decodificação comunica a unidade de execução. A unidade de execução, ao ser informada da instrução que deve ser executada, aciona de forma coordenada as diversas partes do processador para que ocorra a execução da instrução recebida. A ALU, ou unidade aritmético-lógica, é o bloco encarregado de realizar todas as operações aritméticas. As operações que esta unidade realiza são soma, subtração, multiplicação, divisão e as operações lógicas, como AND, OR, NOT, XOR. Buscar instrução na RAM -----> Decodificar a instrução -----> Buscar os operandos | V Armazenar o resultado <----- Executar a instrução
A unidade de controle Para realizar as tarefas indicadas no fluxograma, os mais diversos elementos da unidade de controle do microprocessador precisam realizar tarefas específicas. Os elementos mais importantes são:
O contador Os registradores O decodificador O clock O sequenciador
Se as instruções armazenadas na memória são executadas numa determinada ordem, sequencialmente ou com saltos, é óbvio que precisa existir um componente que indique o endereço de memória onde se encontra a instrução que deve ser executada. É o contador do programa.
A própria instrução que foi obtida da memória precisa ser armazenada no interior do processador para poder ser analisada e executada. Para isto existem componentes chamados registradores de instruções. As instruções são compostas por um código e, na maioria das vezes, por operandos (valores ou endereços de memória). O decodificador, da unidade de decodificação, precisa comparar a instrução que está no registrador com o conjunto de instruções que pertencem ao processador (cada modelo possui um conjunto particular) e ativar o sequenciador que ativa os outros elementos responsáveis pela execução. O ritmo de trabalho é dado por um clock que emite sinais elétricos numa frequência constante. Estes impulsos marcam os instantes em que os passos de cada instrução devem ser executados. A ordem seguida na execução de uma instrução é ditada por um elemento sequenciador que, no ritmo do clock, gera as ordens necessárias para completar a instrução passo a passo. São as chamadas micro-ordens.
A Unidade Aritmético-Lógica (ALU) Os principais elementos da ALU, responsável pelos cálculos aritméticos (soma, subtração, multiplicação e divisão) e pelas operações lógicas (comparações) são:
Circuito Operacional Registradores de Entrada (REN) Acumulador Registrador de Estado ( flags ou sinalizadores)
O circuito operacional contém os circuitos necessários para realizar as operações com os dados procedentes dos registradores de entrada (REN). Estes registradores armazenam os dados ou endereços de memória onde se localizam os dados necessários para que o circuito operacional possa efetuar as operações de acordo com a instrução recebida.
Apesar do nome, estes registradores também são utilizados para armazenar resultados intermediários das respectivas operações. O acumulador é um registrador que armazena os resultados das operações efetuadas pelo circuito operacional. Está conectado com os registradores de entrada para realimentação nos casos de operações em cadeia. Também está diretamente conectado ao bus interno para poder enviar os resultados para a memória ou para a unidade de controle. O registrador de estado, como o nome indica, guarda as condições resultantes da última operação realizada e que possam ser necessárias para as operações subsequentes. Cada um dos seus bits é chamado de sinalizador (ou flag) e indica, por exemplo, se o resultado foi zero, positivo ou negativo.
O Conjunto de Instruções Cada modelo de processador possui um conjunto de instruções próprio. De acordo com o número de instruções que compõem o conjunto, podemos classificar os processadores em CISC e RISC. CISC é a sigla para Computador de Conjunto Complexo de Instruções (Complex Instructions Set Computer) e RISC é a sigla para Computador de Conjunto Reduzido de Instruções (Reduced Instructions Set Computer). Os processadores CISC possuem um número muito maior de instruções (algumas centenas) do que os processadores RISC (algumas dezenas), porém são mais lentos. Os primeiros microprocessadores da Intel eram do tipo CISC e, à medida que evoluíram, foram incorporando algumas características RISC. Quanto mais evoluído um microprocessador, maior é o número de instruções que consegue executar. Basicamente, as instruções podem ser classificadas em:
Instruções de transferência de dados Instruções de cálculos Instruções de transferência de controle do programa Instruções de controle
As instruções de transferência de dados permitem movimentar dados dentro do processador - entre registradores - ou movimentar dados entre a memória e o processador. As instruções de cálculos possibilitam efetuar os cáculos aritméticos e lógicos, assim como o deslocamento e a rotação bit a bit. As instruções de transferência de controle do programa permitem interromper a sequência linear do programa e saltar para um outro ponto do mesmo. As instruções de controle são instruções especiais que atuam sobre o próprio microprocessador, desativando interrupções, bloqueando sua atividade ou passando ordens ao co-processador. As instruções são compostas por um código identificador e, na maioria das vezes, por um ou mais operandos. Considere-as como sendo um jogo completo de informações para que a instrução possa ser realizada. Por exemplo, se for uma instrução de soma, esta vem acompanhada por dois operandos que devem ser somados.
Registradores são áreas de trabalho especiais dentro do processador que são mais rápidas que operandos de memória. Estas áreas foram projetadas para trabalharem com códigos operacionais. Os registradores de um processador Intel ou compatível representam um recurso muito limitado quando se escreve em assembly. Existem apenas 8 registradores de uso geral: EAX, EBX, ECX, EDX, ESI, EDI, ESP e EBP. Na maioria dos casos, ESP e EBP não deveriam ser utilizados porque são usados principalmente para entrada e saída de procedimentos. Na prática, isto significa que você possui apenas 6 registradores de 32 bits para escrever seu código, além de outras localizações de memória que sejam úteis ao procedimento. ESI e EDI podem ser usados normalmente na maior parte das vezes, porém nem um dos dois pode ser acessado em nível de BYTE. Você pode fazer a leitura do WORD menos significativo (baixo) de ESI como SI e do WORD menos significativo de EDI como DI. É muito importante entender o tamanho dos registradores e os dados que eles podem armazenar. Um processador de 32 bits (Intel ou compatível) tem três tamanhos nativos de dados que podem ser utilizados pelas instruções normais com números inteiros: BYTE, WORD e DWORD que correspondem a 8 bits, 16 bits e 32 bits. Isto pode ser mostrado através da notação HEXAdecimal. BYTE 00 WORD 00 00 DWORD 00 00 00 00
Em termos de registradores, isto corresponde aos três tamanhos que podem ser endereçados com os registradores normais para números inteiros. Os processadores Intel são compatíveis com código mais antigo que ainda usa registradores de 8 e 16 bits. Esta compatibilidade é obtida acessando qualquer um dos registradores de uso geral de três
modos diferentes. Usando o registrador EAX como exemplo: AL ou AH = 8 bits AX = 16 bits EAX = 32 bits
Este é o esquema de um registrador de 32 bits de uso geral. Considerando o registrador EAX como exemplo: EAX - Double word
words do double word: Word O.A. (word de ordem alta ou word mais significativo) e Word O.B. (word de ordem baixa ou word menos significativo) que está em AX
bytes do double word: Byte O.A. (byte de ordem alta ou mais significativo, Byte 2, Byte 1 (que está em AH) e Byte O.B. (byte de ordem baixa ou byte menos significativo) que está em AL
Este esquema é mais fácil de entender em nível de bit. Lendo da direita para a esquerda, você tem 32 bits no registrador (bit 0 a 31). Devido à posição dos bits de cada porção de dados que pode ser acessada num registrador de 32 bits, AL é chamado de byte menos significativo ou baixo (LOW byte), AH é chamado de byte mais significativo ou alto (HIGH byte) e AX é chamado de palavra (WORD) menos significativa (LOW word). Existem também oito registradores de 80 bits que são utilizados para ponto flutuante e para a execução de instruções MMX de 64 bits. Processadores mais atuais também possuem oito registradores XMM de 128 bits XMM capazes de utilizar plenamente as instruções SSE e SSE2.
Como usar os registradores Algumas instruções usam determinados registradores para realizar tarefas em particular; algumas instruções são mais rápidas se determinados registradores forem usados; em processadores mais antigos nem todos os registradores podem realizar todas as tarefas que os registradores de processadores mais atuais são capazes de realizar. Estes três fatores, associados ao uso tradicional que os programadores estabeleceram ao longo dos anos, acabaram ditando o modo como os registradores devem ser utilizados. Não é lei, são regras que podem ajudar muito. Veja abaixo:
Use EAX para passar dados para um procedimento e para retornar dados do procedimento para o código que fez a chamada. As APIs do Windows também usam EAX para retornar um valor para o chamador. AL, AX e EAX também deveriam ser usados, na medida do possível, para receber e para transferir dados de e para a memória, por que são ligeiramente mais rápidos que outros registradores. Por exemplo, use MOV AL,[ESI] ao invés de MOV DL,[ESI].
Também nos casos de ADD, AND, ADC, CMP, MOV, OR, SUB, TEST, XCHG, XOR com um valor imediato (isto é, um número como MOV AL,23h), se possível, use AL, AX ou EAX, por que o número de códigos operacionais destas instruções é menor do que se usadas com outros registradores. Use o registrador EDX como um "estepe" para EAX se este estiver em uso. Use o registrador ECX como contador. JECXZ é uma instrução especial que indica se o valor de ECX é zero e a série de instruções LOOP, SCAS e MOVS usam ECX como contador. Use EBX para armazenar dados em geral ou para endereços de memória, por exemplo MOV EAX,[EBX] ou MOV [EBX],EDX. Use ESI quando precisar ler a memória, por exemplo MOV EAX,[ESI], e EDI quando precisar escrever na memória, por exemplo MOV [EDI],EAX. Isto é consistente com as instruções LODSD, STOSD e MOVSD. Use qualquer um dos registradores como base ou registrador index em instruções de memória complexas, por exemplo MOV EAX,[MemPtr+ESI*4+ECX]. Nunca use ESP para outra coisa que não seja um ponteiro da pilha, a não ser que sua rotina não tenha absolutamente nenhuma atividade de pilha. Neste caso você pode salvar o valor de ESP na memória e restaurá-lo antes de voltar para a rotina chamadora. Tradicionalmente o EBP é usado para endereçar dados locais na pilha em rotinas de callback. O EBP e seu componente de 16 bits BP podem ser usados como um registrador geral na programação Windows, mas você deve tomar muito cuidado se estiver usando frames de pilha (stack frames - FRAME no GoAsm) ou dados locais (LOCAL no GoAsm). Isto porque parâmetros de frames de pilha e dados locais são endereçados usando o valor positivo ou negativo de EBP. Depois do EBP ter sido alterado, os parâmetros e os dados locais não podem ser acessados até que o valor original de EBP tenha sido restaurado. CS, DS e SS ainda são utilizados pelo Windows, mesmo se o código for de 32 bits, por isso não devem ser usados. Atualmente, nem ES, FS ou GS podem ser utilizados. Depois do Windows 98 isto gera uma exceção. Se precisar usar os registradores comuns para armazenar uma informação de 64 bits use EDX:EAX, onde EDX guarda os bits mais significativos. Isto está de acordo com as instruções shift de 64 bits SHLD e SHRD, como também com CDQ.
Algumas dicas Para ler o primeiro BYTE do registrador (bits 0 a 7), use mov valorDoByte, al para mover o valor do tamanho do 1o. byte para uma variável. Para ler o segundo BYTE do registrador (bits 8 a 15), use mov valorDoByte, ah para mover o valor do tamanho do 2o. byte para uma variável. Se você quiser ler a primeira palavra (WORD) do registrador (bits 0 a 15), use mov valorDoWord, ax para mover o valor do 1o. word para uma variável. Para obter os bits 16 a 31, você precisa rodar (rotate) os bits no registrador para que possam ser acessados pela instrução acima. Rodando um registrador de 32 bits em qualquer direção em 16 bits vai deslocar os 16 bits de LOW para HIGH e os 16 bits de
HIGH para LOW. Use rol eax, 16 para rodar EAX para a esquerda em 16 bits ou ror eax, 16 para rodar EAX para a direita em 16 bits. Você precisa usar os tamanhos corretos de dados para colocá-los num registrador e não pode misturar tamanhos diferentes de registradores. Por exemplo, mov eax, cl não funciona porque eax é de 32 bits e cl é de 8 bits. Se você precisar por o valor de CL em um registrador de 32 bits, primeiro precisa converter este valor usando várias técnicas diferentes: movzx eax, cl move um inteiro zero estendido sem sinal e movsx eax, cl move um inteiro estendido com sinal. Em alguns casos você pode usar xor eax, eax para zerar (limpar) eax e mov al, cl para copiar cl para al. E ainda existem alguns mnemônicos mais antigos que farão a conversão: mov al, cl copia cl para al, cbw converte o BYTE em AL para um WORD em AX e cwde converte um WORD em AX para um DWORD em EAX.
A arquitetura dos processadores da família Intel é formada por registradores de uso geral, registradores de segmento, registradores de status, ponteiros e registradores de pilha.
Registradores de uso geral Registradores de Uso Geral 16 bits 32 bits AX
EAX AH | AL Acumulador
BX
EBX BH | BL
Base
CX
ECX CH | CL
Contador
DX
EDX DH | DL
Dados
Os usos mais comuns dos Registradores de Uso Geral são: EAX, AX, AH, AL: Chamado de registrador Acumulador. É usado para acessar portas de entrada/saída, operações aritméticas, chamada de interrupções, etc. EBX, BX, BH, BL: Chamado de registrador Base. É usado como um ponteiro base para acessar a memória. Recebe alguns valores
de retorno. ECX, CX, CH, CL: Chamado de registrador Contador. É usado como contador de loop e para shifts. Recebe alguns valores de interrupções. EDX, DX, DH, DL: Chamado de registrador de Dados. É usado para acessar portas de entrada/saída, operações artiméticas e algumas chamadas de interrupção.
Registradores de segmento Registradores de Segmento
CS
Segmento de Código
DS
Segmento de Dados
SS
Segmento de Pilha (Stack)
ES
Segmento Extra
FS Segmento Extra (acima de 386) GS Segmento Extra (acima de 386)
Os usos mais comuns dos Registradores de Segmento são: CS: Contém o segmento de Código no qual o programa está rodando. Mudar seu valor pode pendurar o computador. DS: Contém o segmento de Dados que o programa acessa. Mudar seu valor pode resultar em dados errados.
ES, FS, GS: Estes registrados de segmento extras estão disponíveis para endereçamento distante (far pointer) como memória de vídeo e outras. SS: Contém o segmento de Pilha usado pelo programa. Algumas vezes tem o mesmo valor que DS. Mudar seu valor pode levar a resultados imprevisíveis, geralmente relacionados aos dados.
Registradores de ponteiros e de pilha Registradores de Ponteiros 16 bits 32 bits SI
ESI
Índice de Origem (Source Index)
DI
EDI
Índice de Destino (Destination Index)
IP
Ponteiro de Instrução
Registradores de Pilha Uso mais comum dos Índices e Ponteiros é:
16 bits 32 bits SP
ESP
Ponteiro da Pilha (Stack Pointer)
ES:EDI EDI DI: BP EBP Ponteiro da Base da Pilha (Base Pointer) Registrador do Índice de Destino. É usado para cópia de strings, cópia e configuração de arrays de memória e para endereçamento distante (far pointer) com ES. DS:ESI EDI SI: Registrador de Índice de Origem. É usado para cópia de strings de de arrays de memória. SS:EBP EBP BP: Registrador ponteiro da Base da Pilha. Contém o endereço base da pilha. SS:ESP ESP SP: Registrador ponteiro da Pilha. Contém o endereço do topo da pilha.
CS:EIP EIP IP: Ponteiro do Índice. Contém o deslocamento (offset) da próxima instrução. Pode ser apenas lido.
Registradores de status (Flags) 11 10 0F 0E 0D 0C 0B 0A 09 08 07 06 05 04 03 02 01 00 | | | | | | | | | | | | | | | | | |--| | | | | | | | | | | | | | | | |-----| | | | | | | | | | | | | | | |--------| | | | | | | | | | | | | | |-----------| | | | | | | | | | | | | |--------------| | | | | | | | | | | | |-----------------| | | | | | | | | | | |--------------------| | | | | | | | | | |-----------------------| | | | | | | | | |--------------------------Step) | | | | | | | | |-----------------------------Interrupção | | | | | | | |--------------------------------| | | | | | |-----------------------------------| | | | | |--------------------------------------| | | | |-----------------------------------------Privilégio I/O (286+) | | | |--------------------------------------------(286+) | | |-----------------------------------------------| |--------------------------------------------------|-----------------------------------------------------Virtual (386+)
CF 1 PF 0 AF 0 ZF SF TF
Carry Flag Flag de Paridade Flag Auxiliar Flag Zero Flag de Sinal Trap Flag (Single
IF Flag de DF Flag de Direção OF Flag de Overflow IOPL Nível de NT Nested Task Flag 0 RF Resume Flag (386+) VM - Flag de Modo
Registradores de status (especiais) Registradores de Status - Registradores Especiais (386+) CR0 Registrador de Controle 0 DR0 Registrador de Debug 0 CR2 Registrador de Controle 2 DR1 Registrador de Debug 1 CR3 Registrador de Controle 3 DR2 Registrador de Debug 2 DR3 Registrador de Debug 3 TR4
Registrador de Teste 4
DR6 Registrador de Debug 6
TR5
Registrador de Teste 5
DR7 Registrador de Debug 7
TR6
Registrador de Teste 6
TR7
Registrador de Teste 7
Registradores de status MSW (Machine Status Word -
286+) 31 30-5 4 3 2 1 0 | | | | | | |--| | | | | |-----| | | | |--------| | | |-----------| | |--------------| |------------------| -----------------------
PE Protection Enable MP Math Present EM Emulação TS Task Switched ET Tipo de Extensão Reservado PG Paginação
Bit 0 - PE (Protection Enable): troca o processador entre modo real e protegido Bit 1 - MP (Math Present): controla o funcionamento da instrução WAIT Bit 2 - EM (Emulation): indica quais funções do coprocessador devem ser emuladas Bit 3 - TS (Task Switched): setado e interrogado pelo coprocessador em tarefas de switches e quando interpretando instruções do coprocessador Bit 4 - ET (Extension Type): indica o tipo do coprocessador no sistema Bits 5 a 30 - Reservados Bit 31 - PG (Paging): indica se o processador usa tabelas page para traduzir endereços lineares em endereços físicos
Override de segmentos As operações ES:, DS:, CS: e SS: são prefixos de "segment override". Por exemplo, o código operacional para ES: é 26. Desta forma, para obter MOV ES:[DI],AL, procurase o código operacional para MOV [DI],AL (8805) e precede-se o mesmo com 26 (268805), transformando-o em MOV ES:[DI],AL.
Registrador Segmento Default Overrides Válidos BP
SS
DS, ES, CS
SI ou DI
DS
ES, SS, CS
Strings DI
ES
Nenhum
Strings SI
DS
ES, SS, CS
Registradores mal documentados Existem registradores nos processadores 80386 e acima que não foram bem documentados pela Intel. Estes são divididos em registradores de controle, de debug, de teste e de segmentação no modo protegido. Ao que tudo indica, os registradores de controle, juntamente com os registradores de segmentação, são usados na programação em modo protegido.
Os registradores de teste foram tirados a partir do Pentium. Os registradores de controle são os CR0 a CR4, registradores de debug são os DR0 a DR7, os registradores de teste são os TR3 a TR7 e os registradores de segmentação no modo protegido são o GDTR (Global Descriptor Table Register), o IDTR (Interrupt Descriptor Table Register), o LDTR (Local DTR) e o TR.
A linguagem Assembly a ser utilizada depende essencialmente do processador ao qual se destina, ou seja, é dependente da arquitetura da CPU. A seguir estão algumas das instruções mais utilizadas para processadores da família Intel. Para que possam ser entendidas perfeitamente, é preciso ter uma boa noção da arquitetura do microprocessador. Caso tenha dúvidas, leia antes "Arquitetura Intel".
Instrução AND AND Sintaxe
and destino,fonte
Lógica
destino <- destino AND fonte
Descrição
AND realiza uma operação lógica AND bit a bit nos seus operandos e põe o resultado no destino.
Flags
CF <- 0 OF <- 0 CF OF PF SF ZF (AF indefinido)
Há 5 modos diferentes de se ANDar dois números: 1. 2. 3. 4. 5.
AND dois registradores AND um registrador com uma variável AND uma variável com um registrador AND um registrador com uma constante AND uma constante com um registrador
Ou seja: variável1 db ? variável2 dw ? and and and and and
cl, dh al, variável1
variável2, si dl, 0C2h variável1, 01001011b
Observe que as constantes estão em notação hexadecimal e binária, as únicas aceitas por que são o único meio de expressar números bit a bit. É claro que a notação hexadecimal
precisa ser convertida em 4 dígitos binários. AND retorna 1 quando ambos os operandos forem 1 , senão retorna zero, conforme a tabela abaixo:
Operando 1 Operando 2 Resultado
Pode-se verificar se um registrador está zerado utilizando a instrução AND, como 1 1 1 em and ecx,ecx. Caso algum bit em ecx estiver setado (valor 1), este mesmo bit 1 0 0 estará setado no resultado e a flag zero (ZF) 0 1 0 estará zerada (valor falso). Se não houver bits setados, o resultado também não terá 0 0 0 bits setados e a flag zero (ZF) recebe o valor 1. Nenhum bit será alterado e ecx mantém o seu valor original. Este é o modo padrão de se checar valores zerados. Uma alternativa para este teste é usando a instrução TEST. Também é possível testar uma variável com uma constante (fazer AND em duas variáveis dá erro!). No caso de and variável1, 11111111b testa-se todos os bits da variável1 com bits setados. Se algum bit da variável1 estiver setado, aparece setado no resultado e ZF = 0; se todos estiverem zerados, continuam zerados no resultado e ZF = 1. O valor da variável1, em ambos os casos, não é alterado. AND também é utilizado em máscaras. Caso se queira testar o bit na posição 0 do registrador ecx, podemos utilizar a instrução and ecx, 00000001b . O estado deste bit (setado ou zerado) será transportado para o resultado enquanto todos os outros serão zerados.
Instrução CMP CMP Sintaxe
CMP destino, fonte
Flags
AF CF OF PF SF ZF
Descrição
Subtrai a fonte do destino, atualiza as flags porém não armazena o resultado.
A fonte pode ser um registrador, um endereço de memória ou um valor. O destino pode ser um registrador ou um endereço de memória. Exemplos: cmp eax, variável1 cmp variável2, TRUE
Instrução DEC DEC Decrementa o valor de um registrador ou de uma variável em 1. Exemplos:
dec eax dec variável1
Instrução INC INC Incrementa o valor de um registrador ou de uma variável em 1. Exemplos: inc eax inc variável1
Instruções de salto Apenas os principais tipos de salto (jump) estão na tabela abaixo:
Asm
Hexa
Descrição
ja
77 ou 0F87
salte se acima (jump if above)
jae
73 ou 0F83
salte se acima ou igual (jump if above or equal)
jb
72 ou 0F82
salte se abaixo (jump if below)
jbe
76 ou 0F86
salte se abaixo ou igual (jump if below or equal)
je
74 ou 0F84
salte se igual (jump if equal)
jg
7F ou 0F8F
salte se maior (jump if greater)
jge 7D ou 0F8D
salte se maior ou igual (jump if greater or equal)
jl
7C ou 0F8C
salte se menor (jump if less)
jle
7E ou 0F8E
salte se menor ou igual (jump if less or equal)
jmp
EB ou E9
salto incondicional
jna
76 ou 0F86
salte se não acima (jump if not above)
jnae 72 ou 0F82 salte se não acima ou igual (jump if not above or equal) jnb
73 ou 0F83
salte se não abaixo (jump if not below)
jnbe 77 ou 0F87 salte se não abaixo ou igual (jump if not below or equal) jne
75 ou 0F85
jng 7E ou 0F8E
salte se não igual (jump if not equal) salte se não maior (jump if not greater)
jnge 7C ou 0F8C salte se não maior ou igual (jump if not greater or equal) jnl 7D ou 0F8D
salte se não menor (jump if not less)
jnle 7F ou 0F8F
salte se não menor ou igual (jump if not less or equal)
jnz
75 ou 0F85
salte se não zero (jump if not sero)
jz
74 ou 0F84
salte se zero (jump if zero)
Instrução MOV MOV Sintaxe
MOV destino, fonte
Flags
nenhuma
Descrição Copia um byte ou word do operando fonte para o operando destino. A instrução MOV transfere (MOVe) o conteúdo da fonte para o destino. Ao se executar a transferência, o conteúdo da fonte fica preservado e o conteúdo do destino é substituído pelo conteúdo da fonte.
Instruções NEG e NOT NOT é uma operação lógica e NEG é uma operação aritmética. Ambas são descritas em conjunto para que as diferenças fiquem claras. NOT alterna o valor de cada bit individual: 1 -> 0 0 -> 1
NOT
NEG
Sintaxe
NOT destino
Sintaxe
NEG destino
Lógica
destino <- NOT (destino)
Lógica
destino <- NEG (destino)
Inverte cada bit do operando destino formando seu Descrição complemento de um. O operando pode ser um byte ou um word.
Flags
Subtrai o destino de 0 (zero) e salva o Descrição complemento de 2 no próprio destino.
Flags
AF CF OF PF SF ZF
nenhuma
NEG subtrai o operando destino de 0 e retorna o resultado a este mesmo destino. O efeito é um complemento de dois do operando. O operando também pode ser um byte ou um word. NEG nega o valor do registrador ou da variável numa operação COM sinal. NEG executa (0 - número), ou seja:
neg eax neg variável1
é o mesmo que (0 - EAX) e (0 - variável1) respectivamente. NEG atualiza as flags da mesma maneira que (0 - número). Se o operando for 0 (zero), a flag de carry (CF) é zerada. Em todos os outros casos, a CF é setada para 1.
Instrução OR OR Sintaxe
or destino,fonte
Lógica
destino <- destino OR fonte
OR realiza uma operação lógica OR INCLUSIVE bit a bit nos seus Descrição operandos e põe o resultado no destino. Todos os bits ativos em qualquer dos operandos estará ativo no resultado.
Flags
CF OF PF SF ZF (AF indefinido)
OR retorna 0 quando ambos os operandos forem 0 , senão retorna 1, conforme a tabela abaixo:
Operando 1 Operando 2 Resultado 1
1
1
1
0
1
0
1
1
0
0
0
OR é usado para ativar bits específicos. No exemplo a seguir, apenas o bit da posição 7 é ativado e os restantes não sofrem alteração: or dl, 10000000b ; ativa o bit da posição 7 (as posições dos 8 bits são de 0 a 7,
em ordem inversa)
OR também pode ser utilizado para checar se um registrador está zerado ou não porque o resultado atualiza o estado da flag de zero (ZF). Por exemplo: or ebx, ebx ; ebx é igual a zero ? jz ...
Instrução POP POP Sintaxe
POP destino
OR Transfere o word do topo da pilha (SS:SP) para o destino e Descrição incrementa SP em dois para apontar para o novo topo da pilha. CS não é um destino válido.
Flags
nenhuma
O destino pode ser um registrador ou um endereço de memória. A pilha é uma área de memória que armazena dados temporariamente. O registrador SP (stack pointer) sempre contém o endereço da localização que corresponde ao topo da pilha. O princípio de funcionamento da pilha é "último a entrar - primeiro a sair". A pilha é utilizada principalmente pelas instruções push, pop, call e return.
Instrução PUSH PUSH PUSH fonte PUSH valor (apenas para 80188+)
Sintaxe
Decrementa SP pelo tamanho do operando (dois ou quatro, valores byte Descrição são estendidos por sinal) e transfere um word da fonte para o topo da pilha (SS:SP).
Flags
nenhuma
A fonte pode ser um registrador, um endereço de memória ou um valor literal. A pilha é uma área de memória que armazena dados temporariamente. O registrador SP (stack pointer) sempre contém o endereço da localização que corresponde ao topo da pilha. O princípio de funcionamento da pilha é "último a entrar - primeiro a sair". A pilha é utilizada principalmente pelas instruções push, pop, call e return.
Instruções REP, REPE e REPNE REP / REPE / REPNE REP (repeat / repetir), REPE (repeat if equal / repetir se igual) e REPNE (repeat if not equal / repetir se não for igual) são prefixos para instruções string que forçarão a repetição das instruções de acordo com as seguintes condições:
Prefixo
ECX
Efeito
rep
decrementa ecx
repetir se ecx não for zero
repe
decrementa ecx
repetir se ecx não 0 e ZF = 1
repz
decrementa ecx
repetir se ecx não 0 e ZF = 1
repne
decrementa ecx
repetir se ecx não 0 e ZF = 0
repnz
decrementa ecx
repetir se ecx não 0 e ZF = 0
REPE e REPZ (repeat if zero / repetir se zero) têm o mesmo efeito. O mesmo acontece com REPNE e REPNZ (repeat if not zero / repetir se diferente de zero).
Instrução SCAS
SCAS SCAS compara AL (ou AX) com o byte (ou word) apontado por ES:[DI] e incrementa (ou decrementa) DI dependendo do valor de DF, a flag de direção. O incremento ou decremento é feito de 1 em 1 para bytes e de 2 em 2 para words. OVERRIDES NÃO SÃO PERMITIDOS. As formas permitidas são: scasb scasw scas BYTE PTR ES:[DI] scas WORD PTR ES:[DI]
Instrução SHL SHL Uso
SHL destino, vezes
Desloca os bits do destino para a esquerda as "vezes" indicadas com zeros colocados à direita. A flag de carry conterá o valor do último bit Descrição deslocado.
Flags
CF OF
O destino pode ser um registro ou um endereço de memória. As "vezes" podem ser valores ou CL.
... 7 6 5 4 3 2 1 0 Descrição Carry ... 1 1 1 1 0 0 0 0 ... 1 1 1 0 0 0 0 0 SHL eax,1
1
... 1 1 0 0 0 0 0 0 SHR eax,2
1
... 0 0 0 0 0 0 0 0 SHR eax,2
0
Instrução SHR SHR Uso
SHR destino, vezes
Descrição
Desloca os bits do destino para a direita as "vezes" indicadas com zeros colocados à esquerda. A flag de carry conterá o valor do último bit deslocado.
Flags
CF OF
O destino pode ser um registro ou um endereço de memória. As "vezes" podem ser
valores ou CL.
... 7 6 5 4 3 2 1 0 Descrição Carry ... 1 1 1 1 0 0 0 0 ... 0 1 1 1 1 0 0 0 SHR eax,1
0
... 0 0 0 1 1 1 1 0 SHR eax,2
0
... 0 0 0 0 0 1 1 1 SHR eax,2
1
Instrução SUB SUB Sintaxe
SUB destino, fonte
Lógica
destino <- destino - fonte
Descrição Subtrai a fonte do destino e o resultado é armazenado no destino. Flags
AF CF OF PF SF ZF
Ambos os operandos podem ser bytes ou words e ambos também podem ser números binários com ou sem sinal.
Instrução TEST TEST Sintaxe
TEST destino, fonte
Lógica
(destino AND fonte)
Descrição
TEST realiza uma operação lógica AND bit a bit nos seus operandos sem alterá-los. Apenas modifica as flags.
Flags
CF <- 0 OF <- 0 CF OF PF SF ZF (AF indefinido)
Esta instrução é uma variação da instrução AND. TEST faz exatamente o mesmo que AND, apenas descarta os resultados obtidos. Não modifica o destino. Isto significa que pode checar coisas específicas sem alterar os dados. Em outras palavras, TEST faz um AND lógico em seus dois operandos e atualiza as flags sem modificar o destino e a fonte. test ebx,ebx jz ...
; EBX é zero ? ; se sim, então salta
Para otimizar a velocidade, quando comparar um valor num registrador com 0, use o comando TEST. Use TEST quando for comparar o resultado de um comando lógico
AND com uma constante imediata se o registrador utilizado for EAX. Também pode ser usado para testar se determinado valor é zero (exemplo: test ebx,ebx seta a flag zero (ZF) se EBX for zero). TEST é muito útil para examinar o status de bits individuais. Por exemplo, o snippet abaixo passará o controle para UM_CINCO_OFF se ambos os bits 1 e 5 do registrador AL estiverem zerados (lembre-se de que os bits são numerados de 0 a 7 em ordem inversa). O status de todos os outros bits será ignorado. test al,00100010b jz UM_CINCO_OFF
; filtre os bits 1 e 5 ; se o bit 1 ou o bit 5 estiverem setados, o resultado ; será diferente de zero
... AMBOS_NAO_OFF: ... UM_CINCO_OFF: ...
TEST oferece as mesmas possibilidades que AND: variável1 db ? variável2 dw ? test test test test test
cl, dh al, variável1 variável2, si dl, 0C2h variável1, 01001011b
Um bom exemplo é para a placa de vídeo. Em modo texto, a tela tem 80 x 25 pixels, perfazendo 2000 células. Cada célula possui um byte de caracter e um byte de atributos. O byte do caracter é o valor ASCII do mesmo. O byte de atributos indica a cor do caracter, a cor de fundo, se o caracter está em alta ou baixa intensidade ou se está piscando. Um byte de atributos tem a seguinte aparência: 7 6 5 4 3 2 1 0
Os bits 0, 1 e 2 (RGB) indicam a cor do caracter. 2 é vermelho (Red), 1 é verde (Green) e 0 é azul (Blue). Os bits X R G B I R G B 4, 5 e 6 (RGB) contém a cor de fundo do caracter, onde 6 é vermelho (Red), 5 é verde (Green) e 4 é azul (Blue). O bit 3 indica alta intensidade e o bit 7 é piscante. Se o bit estiver setado (valor 1), o componente correspondente está ativado. Se o bit estiver zerado, o componente correspondente está desativado. A primeira coisa que chama a atenção é o quanto de memória é economizado pelo fato das informações estarem todas juntas. Claro que seria possível usar um byte para cada uma das características, mas a memória requerida seria de 8 x 2 000 bytes = 16 000 bytes. Se adicionarmos os 2 000 bytes referentes aos caracteres, o total já seria 18 000 bytes. Da forma explicada acima, obtém-se o mesmo resultado com apenas 4 000 bytes, ou seja, uma economia de 75%. Como há quatro telas (páginas) diferentes numa placa com cores, os totais seriam 72 000 (18 000 x 4) contra 16 000 (4 000 x 4). Imagine agora que um dos bytes de atributos esteja no registrador DL - pode-se achar
quais os bits que estão setados, bastando para isto fazer um TEST DL com um padrão de bits específico. Se a flag zero (ZF) for setada, significa que o resultado é zero e que o bit estava zerado. test dl, 10000000b test dl, 00010000b test dl, 00000100b
; está piscando ? ; tem azul no fundo ? ; a cor do caracter é vermelho ?
A flag zero (ZF) indica se o componente está ativo ou desativo. Esta flag não vai mostrar se a cor de fundo é azul, porque o vermelho e o verde do fundo também podem estar setados. Apenas um dos componentes pode ser testado em cada test. E lembre-se: TEST não altera os valores da fonte ou do destino, apenas atualiza as flags.
Máscaras Usaremos o byte de atributos do monitor para exemplificar o uso de máscaras. 7 6 5 4 3 2 1 0
Os bits 0, 1 e 2 (RGB) indicam a cor do caracter. 2 é vermelho (Red), 1 é verde (Green) e 0 é azul (Blue). Os bits X R G B I R G B 4, 5 e 6 (RGB) contém a cor de fundo do caracter, onde 6 é vermelho (Red), 5 é verde (Green) e 4 é azul (Blue). O bit 3 indica alta intensidade e o bit 7 é piscante. Se o bit estiver setado (valor 1), o componente correspondente está ativado. Se o bit estiver zerado, o componente correspondente está desativado. Caso se queira ativar ou desativar determinados bits, sem alterar o valor dos outros, podemos lançar mão de uma máscara AND: and byte_do_video, 10001111b . Lembrando que a instrução AND retorna 1 apenas quando ambos os bits estiverem setados (tiverem valor 1), sabemos que neste caso os bits 4, 5 e 6 serão zerados enquanto que os outros permanecem inalterados. Como os bits zerados correspondem à cor de fundo, esta operação tornou a cor de fundo preta. Se quisermos definir a cor de fundo, precisamos de duas operações. A primeira, uma operação de máscara AND como descrito acima, para fazer a cor de fundo preta zerando os bits desejados sem modificar os restantes. A segunda, uma operação de máscara OR, para ativar os bits desejados sem alterar os restantes (cor de fundo azul): and byte_de_video, 10001111b or byte_de_video, 00010000b
As constantes binárias utilizadas para fazer o AND e o OR são chamadas de máscaras. Estas constantes podem estar no formato binário ou hexadecimal. Por exemplo, and byte_de_video, 10001111b é o mesmo que and byte_de_video, 8Fh e or byte_de_video, 00010000b é o mesmo que or byte_de_vídeo, 10h. Reveja as instruções AND e OR caso ainda tenha alguma dúvida.
Instrução XOR
XOR Sintaxe
XOR destino, fonte
Lógica
destino <- destino XOR fonte
Descrição
XOR realiza uma operação lógica OR EXCLUSIVE bit a bit nos seus operandos e põe o resultado no destino.
Flags
CF OF PF SF ZF (AF indefinido)
XOR retorna 1 quando os operandos forem diferentes , senão retorna 0, conforme a tabela abaixo:
Operando 1 Operando 2 Resultado
Decimal Binário O OUExclusi 1 1 0 77 01001101 vo Lógico 1 0 1 25 00011001 XOR (XOR) 0 1 1 84 01010100 signific Resultado a que o 0 0 0 resultado SÓ É VERDADEIRO SE AS CONDIÇÕES FOREM DIFERENTES. Falso e verdadeiro também podem indicar o estado de bits, portanto, podemos efetuar uma operação de OU-Exclusivo Lógico entre dois bits (como na tabela acima) ou numa sequência de bits. Tomemos como exemplo a operação 77 XOR 25. Como sabemos que a operação lógica XOR também é feita bit a bit, precisamos dos valores binários desses dois números para efetuar a operação: Observe que apenas nas posições onde os bits são diferentes o resultado possui bits com valor 1, portanto, 77 XOR 25 = 84. Um aspecto interessante da operação XOR é que ela é reversível: se fizermos um XOR do resultado com o primeiro operando, obtemos o valor do segundo operando. Da mesma forma, se fizermos um XOR do resultado com o segundo operando, o resultado é o primeiro operando. Outra característica é que, fazendo o XOR de um número com ele mesmo, o resultado sempre será zero. Faça os testes e verifique
A ordem reversa de armazenamentos de endereços é uma característica dos processadores Intel. Este é um assunto que você precisa dominar se pretende programar em Assembly e se quiser examinar dados e a memória do seu computador usando um debugger. Basicamente, os processadores Intel guardam dados na memória "no avesso", ou seja, os dados são armazenados byte a byte em ordem reversa. A coisa se resume no seguinte: quando um byte é armazenado num endereço de memória a sequência de bits é mantida, mas quando um word, composto por dois bytes, é armazenado na memória, seus bytes serão armazenados em ordem reversa. Isto quer dizer que o byte menos significativo (o segundo) será armazenado primeiro, seguido pelo byte mais significativo (o primeiro). Esta sequência "ao contrário" é chamada de formato little endian. A sequência na
ordem correta é chamada de big endian e é usada por outros tipos de processadores. É meio difícil explicar o little endian por meio de palavras - um exemplo torna a coisa bem mais fácil. Digamos que você queira armazenar o valor 248Ch no endereço de memória 400000h. A área de memória teria o seguinte aspecto:
Endereço Valor
Quando este valor for lido da memória por uma instrução word, tendo como referência o endereço 400000h, os bytes serão lidos 0040 0000 8C em ordem reversa e o registrador word (de 32 bits) indicado receberá o valor 248Ch. Se o armazenamento ocorre em ordem 0040 0002 24 inversa e a leitura também, não tem com que se preocupar - o computador está fazendo o serviço direitinho. A única coisa é que precisamos saber disto para que, ao visualizarmos diretamente esta área da memória usando um debugger, saibamos que os bytes estão invertidos. Para reforçar o conceito do mecanismo little endian usado pelos processadores Intel, vou exemplificar o armazenamento de um dword, digamos 12345678h também em 400000h:
Endereço Valor 0040 0000
78
0040 0002
56
0040 0004
34
Agora você sabe o que é o little endian, não vai ficar confuso com ele. Mas por que cargas d'água o pessoal da Intel resolveu adotar este modo de armazenamento? Como tudo na vida, as coisas têm uma explicação lógica
Vamos simplificar o problema para entender a mecânica e a origem do little endian. Imagine a área de memória como uma 0040 0006 12 enorme prateleira vazia. Se você tivesse que armazenar apenas um bit na memória, qual posição você escolheria? Não sei se sou muito folgada, mas se a memória estiver vazia eu escolheria a primeira prateleira livre que estivesse mais em baixo (se eu escolhesse a que estivesse mais em cima teria que buscar uma escada e subir até o topo da prateleira). Agora vamos ampliar nosso modelo teórico: já temos um bit armazenado e queremos guardar um segundo bit. O que é mais fácil? Deslocar o primeiro bit para a segunda prateleira e colocar o segundo bit na primeira ou simplesmente colocar o segundo bit na segunda prateleira? Novamente, movida pela lei do mínimo esforço, eu optaria por colocar o segundo bit na segunda prateleira. Ao invés de pensar em bits, vamos agora pensar em bytes (conjuntos de 8 bits). Se tivermos apenas um byte para colocar na memória vazia, vamos alocá-lo no início; se tivermos um segundo byte, seguindo o raciocínio do modelo dos bits, ao invés de deslocar o primeiro byte para inserir o segundo no início da memória, o segundo byte vai para a segunda prateleira. Taí! Novamente escolhemos a solução mais fácil e rápida... só que, para obter o valor original, vamor ter que ler os bytes na ordem inversa em que foram colocados.
...
...
3 milhar 5 2 centena 4 1 dezena 8
Para aqueles que se embananam com bits e bytes, vou dar mais um 0 unidade 7 exemplo usando o sistema decimal. Sabemos que valores decimais são expressos em unidades, dezenas, centenas, etc. Se quisermos guardar o número 5.487 na prateleira da memória, começamos guardando as 7 unidades, depois as 8 dezenas, as 4 centenas e finalmente os 5 milhares. Nós e o computador guardamos os "pedaços do número" desta forma, de baixo para cima. Na hora de recuperar o valor original, o computador busca os "pedaços" na mesma ordem em que foram colocados na prateleira, ou seja, 7845. Como a máquina sempre guarda e lê as coisas da esquerda para a direita (ou de baixo para cima, como quiser), o valor original fica preservado. Acontece que o cérebro dos humanos foi condicionado a ver o número na ordem inversa, ou seja, 5487 precisa ser lido da esquerda para a direita (ou de cima para baixo). Ao invés de mudar a arquitetura da máquina para imitar um costume dos humanos, o little endian facilitou as coisas para o computador e deixou por conta dos humanos a inversão da leitura. Não existe um modo mais certo ou mais errado de armazenar dados. Existe apenas um estilo e o pessoal da Intel escolheu o que acabei de explicar. Para dar nomes aos bois, este estilo foi chamado de little endian. Por dedução, obig endian é o estilo que desloca o primeiro para inserir o segundo byte. Dá um pouco mais de trabalho, exige um pouco mais de processamento, mas também dá certo. O pessoal da IBM escolheu usar o big endian. Com isto facilitou as coisas para os humanos exigindo um pouco mais da máquina. Certo ou errado? Diria que é apenas uma questão de estilo...
Página 1 de 3
A tradução literal de "flag" é bandeira. Não foi por acaso que as "flags" do processador receberam este nome: funcionam como sinalizadores. Vou manter o nome flag e não usar algum tipo de tradução por que esta denominação já foi incorporada ao "computês" do Brasil e não vale a pena discutir ou mudar um hábito. Neste tutorial, aprenda a usar as flags para criar saltos condicionais. As flags são apenas um único bit de memória localizado dentro do processador. Como cada flag é apenas um bit, num dado momento elas só podem ter o valor 1 ou 0 (flag "setada" ou "zerada"). Existem flags que podem ser usadas para indicar o resultado de certas instruções. Algumas instruções como CMP, TEST e BT não fazem outra coisa a não ser alterar algumas destas flags, outras realizam tarefas adicionais além de alterar algumas das flags. Também existem instruções que simplesmente não alteram as flags. Um uso comum das flags é o de desviar a execução para um ponto em particular do código usando instruções de salto condicinal. Estas instruções farão o salto (ou não) dependendo do estado de uma ou de mais flags. Apenas cinco das flags podem ser usadas deste modo - as flags zero, sinal, carry, overflow e paridade. A sexta flag (carry auxiliar) e a sétima (flag de direção) são lidas por outro tipo de instrução. A seguir estão informações adicionais a respeito das cinco flags que podem ser usadas por saltos condicionais.
Z (flag zero) Está setada (tem o valor 1) se o resultado de uma operação for zero. Depois de uma instrução aritmética, se o número deixado no registrador ou na a área de memória objeto da instrução for zero, então a flag é setada. Para obter esta informação geralmente só é preciso fazer uma simples comparação de dois valores sem alterá-los. Neste caso, pode-se usar a
instrução CMP. CMP imita um SUB sem alterar os valores passados como operandos. Por exemplo: CMP SUB CMP CMP
EAX,33h EAX,33h EAX,EDX EAX,[VALOR]
; ; ; ;
flag flag flag flag
zero zero zero zero
= = = =
1 1 1 1
se se se se
eax eax eax eax
= = = =
33h, mas não altera eax 33h (eax diminui em 33h) edx o número em VALOR
A flag zero também pode ser usada para mostrar o resultado de uma contagem crescente ou decrescente, por exemplo:
DEC EAX INC EAX
; flag zero = 1 se eax for zero depois da instrução. ; flag zero = 0 se eax for diferente de zero. ; flag zero = 1 se eax for zero depois da instrução. ; flag zero = 0 se eax for diferente de zero.
A flag zero também pode ser usada para controlar a repetição de instruções de string, isto é, LODS, STOS e MOVS: CMP EAX,0 OR EAX,EAX TEST EAX,EAX
; ; ; ;
ver se o número em eax é zero (flag zero está setada) faz a mesma coisa mas usa 2 opcodes ao invés de 3 faz a mesma coisa e também só usa 2 opcodes
As versões de 16 e 8 bits das instruções testam apenas os primeiros 16 ou 8 bits do registrador ou da área de memória. Por exemplo: CMP W[JOAO],0 CMP B[MARIA],0 OR DX,DX TEST DH,DH SUB B[VALOR],2 DEC SI INC B[CONTA]
; ; ; ; ; ; ; ; ; ;
ver se os primeiros 16 bits da memória marcada como JOAO são zero ver se os primeiros 8 bits da memória marcada como MARIA são zero ver se o registrador dx é zero (16 bits) ver se o registrador dh é zero (8 bits) reduz os 8 bits guardados em VALOR em 2 (flag zero setada se for zero) flag zero setada quando si for zero (16 bits) flag zero setada quando os 8 bits de CONTA são zero
Já que as flags são muito úteis para indicar se houve retorno com sucesso ou não de uma rotina, algumas vezes será preciso setá-las diretamente. Para setar a flag zero pode-se usar: CMP EAX,EAX SUB EAX,EAX CMP EAX,EDX OR EAX,EAX TEST EAX,EAX
; ; ; ; ;
seta a flag zero sem alterar eax seta a flag zero fazendo eax = 0 quando precisam ser diferentes, zera a flag zero quando eax não pode ser zero, zera a flag zero mesmo efeito que OR EAX,EAX
Quando usada com TEST, a flag zero será setada se o bit testado for zero. MOV ECX,1 TEST ECX,1 CMP ECX,1 MOV EDX,0
; põe valor 1 em ecx ; a flag zero não é setada ; a flag zero é setada
TEST EDX,1 CMP EDX,1 MOV EBX,-1 TEST EBX,-1 CMP EBX,-1
; a flag zero é setada ; a flag zero não é setada ; testa todos os 32 bits (flag zero zerada) ; ver se ebx é -1 (flag zero setada)
A flag zero é usada principalmente com as instruções de salto condicional JZ (saltar se for zero) e JNZ (saltar se não for zero) , por exemplo: JZ >L10 JNZ L1
; salta para frente para L10 se flag zero = 1 ; salta para trás para L1 se flag zero = 0
A flag zero também é utilizada com as instruções de salto condicional JA ("jump if above" salte se acima), JB ("jump if below" - salte se abaixo) e instruções semelhantes. Também pode ser usada em um loop utilizando instruções especiais ou apenas a flag, por exemplo: L1: ... CMP EDX,EAX LOOPZ L1 L1: ... CMP EDX,EAX LOOPNZ L1 L1: ... CMP EDX,EAX JZ >L10 LOOP L1 L10: L1: ... CMP EDX,EAX JNZ L1
; outro código aqui ; decrementa ecx, loop continua até que ecx = 0 ; ou até que edx = eax (quando flag zero = 1) ; outro código aqui ; decrementa ecx, loop continua até que ecx = 0 ; ou até que edx seja diferente de eax (quando flag zero = 0) ; outro código aqui ; salte para fora do loop quando edx = eax (flag zero = 1) ; decrementa ecx, loop continua até que ecx = 0
; outro código aqui ; loop continua até que edx = eax (flag zero = 1)
Índice do Artigo Flags e saltos condicionais Flag de Sinal e de Carry Flag de Overflow e de Paridade Todas as páginas Página 2 de 3
S (flag de sinal) Esta flag está setada quando o bit mais significativo (o bit mais à esquerda) do resultado for 1. A posição deste bit depende do tamanho do dado. Num byte, o bit mais significativo é o bit 7 (8° bit dos bits 0 a 7); num word é o bit 15 (16° bit dos bits 0 a 15) e num dword é o bit 31 (32° bit dos bits 0 a 31). Este bit estará setado se o resultado da instrução for 80h ou mais (para um byte), 8000h ou mais (para um word) ou 80000000h ou mais (para um dword). Lembre-se que, em números com sinal, o bit mais significativo indica se o número é negativo ou não. A flag de sinal é alterada por INC e DEC, instruções que não alteram a flag de carry. Por isto pode ser muito útil testar a flag de sinal em loops. Por exemplo: L0: ; DEC ECX JNS L0
; reduz ecx em um ; volta para L0 se ecx ainda não for -1
Seu uso também é muito conveniente em funções de multi-ação, por exemplo: MULTI_ACAO: DEC AL JS >L0 DEC AL JS >L1 DEC AL JS >L2 DEC AL JS >L3 DEC AL JS >L4
; ; ; ; ; ; ; ; ; ; ;
na entrada, al guarda a ação desejada ver se al contém zero se sim, saltar para L0 ver se al contém um se sim, saltar para L1 ver se al contém dois se sim, saltar para L2 ver se al contém três se sim, saltar para L3 ver se al contém quatro se sim, saltar para L4
A flag de sinal também é um jeito muito prático de ver se o bit alto de um registrador está setado ou zerado. Várias instruções setam a flag sem alterar o registrador, por exemplo: OR EDX,EDX CMP EDX,EDX TEST EDX,EDX OR CL,CL CMP CL,CL TEST CL,CL
; seta a flag de sinal se o bit alto de edx estiver setado ; - idem ; - idem ; seta a flag de sinal se o bit alto de cl estiver setado ; - idem ; - idem -
Ao checar áreas de memória, pode-se endereçar a memória apenas uma vez por instrução. Desta forma, é preciso usar CMP, como por exemplo: CMP B[DATA44],0 setado CMP W[DATA44],0 setado
; seta a flag de sinal se o 8° bit de DATA44 estiver ; seta a flag de sinal se o 16° bit de DATA44 estiver
CMP D[DATA44],0 setado CMP B[DATA44+7],0 setado CMP B[DATA44+9],0 setado
; seta a flag de sinal se o 32° bit de DATA44 estiver ; seta a flag de sinal se o 64° bit de DATA44 estiver ; seta a flag de sinal se o 80° bit de DATA44 estiver
Observe que a posição do bit mais alto na área de memória chamada de DATA44, usada nestas instruções, depende do tamanho do dado usado na instrução. Isto é porque dados em áreas de memória são armazenados com os bytes em ordem reversa, ou seja, o byte menos significativo primeiro e o mais significativo por último. Veja Little Endian. A instrução CMP B[DATA44+7],0 analisa o 8° byte que contém o 64° bit. Este é o sinal, mas apenas para um dado de 64 bits de tamanho. A flag de sinal é usada principalmente com as instruções de salto condicional JS e JNS, por exemplo: JS >L10 JNS L1
; salta para frente para L10 se a flag de sinal = 1 ; salta para trás para L1 se a flag de sinal = 0
A flag de sinal também é usada com as instruções de salto condicional JG ("jump if greater-than" - salte se maior que), JNG ("jump if not greater-than" - salte se não maior que) e semelhantes.
C (flag de carry) Esta flag é setada quando o resultado da instrução estourou o limite do tamanho do dado, isto é, houve uma transposição ("carry"), o famoso "vai-um". Imagine, por exemplo, que numa instrução de 8 bits o valor 1 é adicionado a 255. O resultado NÃO pode ser 256 por que 255 é o valor máximo que pode estar contido num byte de 8 bits. O resultado será 0, mas a flag de carry será setada. Imagine também que numa instrução o valor 4 é subtraído de 2. Novamente isto faz com que a flag de carry seja setada (tenha valor 1) por que o resultado caiu abaixo de zero, que é o limite inferior do tamanho do dado. A flag de carry, portanto, indica que uma transposição ou estouro (overflow) ocorreu quando se estava usando números sem sinal. Veja a flag de overflow para encontrar estouros quando são usados números com sinal. Diferentemente de outras flags, existem instruções criadas para manipular a flag de carry diretamente. STC CLC CMC
; seta a flag de carry ; zera a flag de carry ; complementa a flag de carry
Como estas instruções são muito simples, a flag de carry é muito útil para levar o resultado de uma função ao seu chamador, por exemplo: CALCULA2: ;
CMP EBX,ESI JZ >.falhou CMP EAX,ESI JZ >.sucesso .falhou STC RET .sucesso CLC RET ; CALCULA1: CALL CALCULA2 JC >L40
; salta para falhou se ebx = esi ; salta para sucesso se eax = esi ; seta a flag de carry para mostrar falha
; zera a flag de carry para indicar sucesso
; salta para frente para L40 se falhou
As instruções INC e DEC não alteram a flag de carry. As instruções de loop também não. Isto é útil quando se tem um loop que precisa informar seu resultado. Neste caso também podemos usar a flag de carry, como por exemplo: .loop ; CMP ESI,EDI DEC ECX JNZ .loop RET
; ; ; ;
ver se esi é menor que edi (seta carry se for) ver se mais loops precisam ser feitos sim returna com o resultado de cmp esi,edi na flag de carry
Existem algumas instruções que sempre zeram a flag de carry, o que é bom saber para evitar o uso desnecessário da instrução CLC. Estas instruções são AND, OR e TEST. Algumas instruções respondem a um input da flag de carry; outras fazem seu output na flag de carry. A flag de carry é usada principalmente com as instruções de salto condicional JC e JNC, por exemplo: JC >L10 JNC L1
; salta para frente para L10 se a carry estiver setada ; salta para trás para L1 se a carry estiver zerada
A flag de carry também é utilizada com as instruções de salto condicional JA ("jump if above" - salta se acima), JB ("jump if below" - salta se abaixo) e similares.
O (flag de overflow) Overflow significa
transbordar, inundar. No "computês" do Brasil costuma-se dizer "estourar". Para entender a flag de overflow é preciso que fique bem claro o que são números com sinal. A flag de overflow é usada para indicar um estouro quando números com sinal são usados. A flag de carry não pode ser usada para este fim. Um simples exemplo é suficiente para provar: MOV AL,0FEh ADD AL,4h
; põe 254 decimal (sem sinal) ou -2 (com sinal) em al ; adiciona 4. al agora está com 2h
Neste caso a flag de carry será setada por que o resultado sem sinal 258 ultrapassa o limite de 255 que corresponde ao tamanho do dado. Porém, se este for um cálculo com sinal, não
haverá estouro: -2 + 4 = 2, al contém o resultado correto de 2 e a flag de overflow permanece zerada. Um outro exemplo onde há um overflow num cálculo com sinal: MOV AL,7Fh ADD AL,4h
; põe 127 decimal em al ; adiciona 4. al agora está com 83h
Neste outro caso a flag de carry está zerada por que o resultado sem sinal 131 está dentro do limite de 255 do tamanho do dado. Porém, em relação ao cálculo com sinal, houve um estouro por que, se al contém 83h, este é o número decimal com sinal -125, um resultado errado. O resultado correto 131 fica fora dos limites -127 a +128 dos números com sinal de 8 bits. Neste tipo de operação aritmética o processador seta a flag de overflow se o bit de sinal muda sem que tenha havido um "carry". Observe como isto ocorre independente da flag de carry: MOV AL,7Fh INC AL ; MOV AL,80h DEC AL
; põe 127 decimal em al ; causa overflow (carry não é afetada por INC) ; põe -128 decimal em al ; causa overflow (carry não é afetada por DEC)
Nas instruções de deslocamento (shift), apenas nas operações de um único shift pode-se esperar que a flag de overflow dê uma indicação válida se um resultado com sinal for muito grande para o tamanho do dado. Por exemplo: MOV AL,80h SHL AL,1 MOV AL,0FEh SHL AL,1 MOV AL,80h SHL AL,2 MOV AL,0FEh SAR AL,1
; ; ; ; ; ; ; ;
põe -128 em al multiplicar por 2 causa overflow põe -2 em al multiplicar por 2 resulta em -4 sem overflow põe -128 em al multiplica por 4 sem indicação de overflow põe -2 em al divisão por 2 resulta em -1 sem overflow
SAR é uma instrução especial de deslocamento com sinal à direita que mantém o sinal correto no resultado. Ela consegue fazer isto deslocando todos os bits, exceto o bit mais alto. Como um shift SAR único na realidade é uma divisão por +2, nunca poderá ocorrer um overflow. Já a instrução SHL pode e, em operações de um shift único, a flag de overflow será devidamente setada. Para isto, o processador faz um teste para ver se o bit de sinal é o mesmo que a flag de carry, zerando a flag de overflow se seus valores forem idênticos. Devido a este teste é possível estabelecer um outro uso para a flag de overflow (estes testes mudam o conteúdo do registrador): SHL AL,1 JNO >L1 SHL AL,1 JO >L1 SHL EAX,1 JNO >L1 SHL EAX,1 JO >L1
; salte se os dois bits mais altos de al forem iguais ; salte se os dois bits mais altos de al forem diferentes ; salte se os dois bits mais altos de ax forem iguais ; salte se os dois bits mais altos de eax forem diferentes
Instruções de rotação funcionam do mesmo modo que as de deslocamento. Como a instrução ROR desloca todos os bits para a direita substituindo o bit mais alto pelo mais baixo, isto possibilita comparar o bit mais alto com o bit mais baixo de dados. Por exemplo (estes testes mudam o conteúdo do registrador): ROR AL,1 JNO >L1 ROR AL,1 JO >L1 ROR EAX,1 JNO >L1 ROR EAX,1 JO >L1
; salte se o bit mais alto e o mais baixo de al forem iguais ; salte se o bit mais alto e o mais baixo de al forem diferentes ; salte se o bit mais alto e o mais baixo de eax forem iguais ; salte se o bit mais alto e o mais baixo de eax forem diferentes
A instrução especial de multiplicação com sinal IMUL seta a flag de overflow se o resultado com sinal for maior que o tamanho do dado. A flag de overflow é usada principalmente com as instruções de salto condicional JO e JNO, por exemplo: JO >L10 JNO L1
; salte para L10 se a flag de overflow estiver setada ; salte para L1 se a flag de overflow estiver zerada
P (flag de paridade) A flag de paridade indica se existe um número par ou ímpar de bits setados no dado. Ela estará setada se o número de bits setados for par e zerada se o número for ímpar. Na comunicação serial, o bit de paridade é usado como um checador de erros pouco sofisticado. Após cada byte enviado, o transmissor envia um bit de paridade que indica ao receptor se o byte que acabou de ser enviado deveria ter um número par ou ímpar de bits setados. Este sistema pode deixar escapar um byte corrompido, mas geralmente detecta uma série de bytes corrompidos. Quando usado desta forma, um byte pode ter menos do que 8 bits: transmissões seriais geralmente usam bytes de 7 bits mais um bit de paridade. A flag de paridade geralmente é usada com as instruções de salto condicional JP e JNP, por exemplo: JP >L10 JNP L1
; salte para L10 se a flag de paridade estiver setada ; salte para L1 se a flag de paridade estiver zerada
Uma das minhas ferramentas favoritas para criar programas em Assembly é o macroassembler de Microsoft, o famoso masm32. Atualmente o masm32 faz parte do MASM32 Project, supervisionado por Steve Hutchesson. O software é gratuito e pode ser usado com algumas poucas restrições (leia a licença) - a mais importante delas é que não pode ser utilizado para criar software ilegal como vírus e ferramentas para hacking. Há muitos anos Steve Hutchesson, conhecido como hutch, disponibiliza pacotes completos baseados no masm32 que incluem assembler, linkeditor, editor de código fonte, editor e compilador de recursos, sistema de ajuda com as principais referências de
funções e códigos operacionais, etc e tal. Há mais de 10 anos lembro-me de ter instalado a versão 6 deste pacote, que hoje está na versão 10. Este australiano tem a mesma idade que eu (somos de 1948) e sempre foi uma figura muito polêmica no cenário da programação assembly. Admirado ou odiado, não me interessa, o fato é que graças a ele os pacotes masm32 podem ser baixados e utilizados por milhares de profissionais e estudantes que se interessam pela linguagem Assembly. Ah, falando nisso, na seção de downloads da Aldeia você encontra as versões 8, 9 e 10 do dito pacote e, para conferir se existe alguma versão mais atual, visite o site do hutch.
Instalando a versão 10 do masm32 Muitos visitantes da Aldeia já fizeram contato para esclarecer uma dúvida: porque o masm32 dá uma mensagem de erro dizendo que O Windows não pode acessar o dispositivo, caminho ou arquivo especificado. Talvez você não tenha as permissões adequadas para acessar o item quando tentam
criar um executável? A resposta é bastante simples. Antes da versão 10 era preciso configurar o masm32 "na unha" para determinar o caminho dos arquivos fonte. A partir desta versão, a configuração é feita durante a instalação e você pode esquecer o assunto. Este é o motivo pelo qual escolhi esta versão como exemplo de instalação. Baixe o pacote colocando-o numa pasta qualquer. Dê uma duplo clique no arquivo m32v10r.zip para descompactá-lo. A primeira telinha que aparece é a mostrada abaixo. Escolha o drive onde você quer instalar todos os componentes do SDK e despois clique no botão Start (no meu caso, escolhi o drive D).
Logo em seguida aparece um aviso (típico do hutch) cuja tradução é a seguinte:
Alguns antivírus não tiveram um escaneamento heurístico devidamente programado e produzem falsos positivos quando escaneiam arquivos muito pequenos, comuns na programação assembly. Infelizmente isto é consequência de uma demanda comercial apressada e da falta de habilidade de programação de alguns fornecedores de AV, os quais tentam impor um subconjunto das especificações Microsoft Portable Executable de arquivos executáveis do
Windows 32 bits para encobrir algumas das suas limitações técnicas. O MASM32 SDK foi construído num ambiente totalmente isolado a partir da sua fonte original em formato texto, foi instalado com sucesso em milhões de computadores no mundo todo e não contém qualquer infecção viral ou código de trojans. Se a sua instalação for danificada ou sofrer qualquer interferência de um antivírus, você precisa alterar sua configuração para que ele não delete ou danifique arquivos durante a instalação do MASM32.
Entendido o aviso, clique no botão Ok.
Novamente aparece uma aviso:
Esta instalação NÃO foi programada para ocorrer sem assistência OU no background. Ela realiza operações que exigem muito do processador para construir as bibliotecas e pode não funcionar corretamente ou deixar de criar as bibliotecas se rodar sem assistência ou em baixa prioridade.
Novamente, entendido o aviso, clique em Ok. Finalmente, depois desta longa introdução das encrencas que podem acontecer, peça para descompactar o pacote. Espero que tudo corra bem, assim como aconteceu comigo. Se sim, uma pasta masm32 foi criada no drive da sua escolha e uma porção de subpastas e arquivos foram colocados neste local Falta criar as bibliotecas e... adivinhe o quê? Lá vem novo aviso!
Este diz o seguinte:
Construir as bibliotecas para o MASM32 SDK é uma operação no modo console. Esta tarefa não deve sofrer interferências de nenhum outro processo por que isto pode gerar erros na criação das bibliotecas. Encerre qualquer tarefa que consuma processamento antes de prosseguir. Você deve monitorar o processo de construção das bibliotecas para garantir que ele seja completado sem problemas. NOTE que, enquanto as bibliotecas da API do Windows estão sendo criadas, pequenos alertas com informações sobre a construção são mostrados e podem ser ignorados.
Terminada esta fase, uma telinha preta (típica da área DOS do Windows), deve mostrar alguma coisa do tipo:
Se tudo correu bem, de acordo com o previsto, você será agraciado com a seguinte mensagem:
Agora falta pouco. Basta responder mais uma pergunta com Yes
para obter a última telinha sobreposta no editor que traz uma porção de informações:
Clique em Close e isto é tudo. Se quiser ver como um programinha é montado, clique no item de menu do editor [File / Open] e abra o código fonte de alguns programas que estão nas pastas masm32/tutorial e masm32/examples.
Índice do Artigo O folgado (masm) O executável Todas as páginas Página 1 de 2
Aprender fazendo, este é o segredo. Neste tutorial ainda tem muita teoria e o primeiro resultado é, no mínimo, frustrante. Mas não desanime: veja como fazer a API do Windows trabalhar para você.
Projeto Vamos realizar um projeto extremamente simples, copiado descaradamente do tutorial 3 do Iczelion. A idéia é genial: um programa cuja única função é retornar ao Windows, ou seja, dá a impressão de que não faz nada - um folgado. Apesar disso, é um programa! Na verdade, o que interessa realmente são os conceitos necessários para programar "o folgado". Sei que ainda é muita teoria para pouca prática, mas o começo é assim mesmo.
Planejando o folgado
Vamos escrever um programa que tem apenas uma linha de código. Através deste projeto teremos a oportunidade de rever a estrutura de um programa, além de ampliar nossos conhecimentos. Então vamos planejar: 1. Nosso sistema operacional é o Windows de 32 bits (ou 64, se você é dos apressadinhos). 2. Não precisamos mais do que o conjunto de instruções do processador 386. 3. Temos o MASM à disposição para fazer o trabalho pesado. 4. O objetivo do programa é voltar para o Windows assim que for executado. Nada mais
Botando a mão na massa Chame o QEditor do MASM e digite o seguinte: .386 .MODEL FLAT,STDCALL .CODE inicio: end inicio
O sistema operacional win32 possui uma quantidade muito grande de funções que ele utiliza - nada impede que a gente pegue uma carona e também as usemos. Esta imensa coleção de funções do win32 é chamada de API(Application Programming Interface). As funções estão organizadas em bibliotecas denominadas bibliotecas de vínculo dinâmico (dynamic-linked libraries) ou DLL. Três delas são as mais importantes e as mais utilizadas: kernel32.dll, user32.dll e gdi32.dll. A kernel32.dll contém funções API que lidam com a memória e com a administração de processos. A user32.dll possui funções que controlam a aparência da interface com o usuário e a gdi32.dll tem funções responsáveis por operações gráficas. Caso exista uma função na API win32 que execute exatamente o trabalho que pretendemos realizar, podemos usá-la diretamente no nosso programa ao invés de escrever todo o procedimento. A função que precisamos chama-se ExitProcess e está localizada na biblioteca kernel32.lib. Dando uma olhada na referência da API do Windows, encontramos o seguinte: VOID ExitProcess( UINT uExitCode // código de saída para todos as linhas de execução (threads) );
Esta função não tem valor de retorno (é VOID) e exige um parâmetro (uExitCode) do tipo UINT. UINT é apenas um dos muitos nomes que o Windows usa para uma DWORD (double word - palavra de 32 bits). Para poder usar esta função, é preciso passar algumas informações para o assembler e para o linker: o nome da função e a biblioteca onde ela está . Com estas informações, o assembler/linker indicará ao executável onde ele deve buscar a função que deve ser
executada. O código da função NÃO é adicionado ao nosso executável, apenas as informações de qual função (ExitProcess) deve ser executada e onde encontrá-la (na kernel32.dll) em tempo de execução. De posse dessas informações, nosso programa será capaz de executar uma operação de chamada, ou seja, um call ExitProcess. A instrução call faz parte do conjunto de instruções do 386 e funciona da seguinte maneira:
Antes de incluirmos a instrução call ExitProcess no nosso código, precisamos colocar os parâmetros correspondentes na pilha. A pilha é um registrador especial da CPU, cujo conteúdo indica o endereço da memória onde se deposita uma informação que deva ser temporariamente guardada no curso do processamento. No nosso exemplo, é apenas o parâmetro uExitCode, o valor que o Windows recebe quando o nosso programa termina. Para isto usamos a instrução push (empurre para a pilha) e o fragmento de código fica assim: push 0 call ExitProcess
Se esquecermos de "pushar" o valor do parâmetro para a pilha, o assembler/linker não irá notar a falta. Usamos uma instrução do conjunto de instruções do 386 e o assembler não tem como checar os parâmetros (quem usa a instrução deve saber o que está fazendo...) ao produzir o executável. Você só vai notar o erro quando executar o programa que, logicamente, dá pau. Existe um modo mais seguro e cômodo de fazer chamadas. É através do INVOKE, uma sintaxe de chamada de alto nível. A sintaxe de INVOKE é a seguinte: INVOKE expressão[,argumentos]
onde expressão pode ser o nome de uma função (ou um ponteiro para a função) e os argumentos (parâmetros) são separados por vírgulas. Neste caso o assembler/linker assembler/linker tem condições de verificar a sintaxe porque os parâmetros são citados explicitamente (e não apenas "pushados" para a pilha). Dá para perceber que um pouquinho de alto nível no assembly não faz mal a ninguém. Só tem um porém... para poder utilizar o INVOKE é necessário fornecer um protótipo da função que se quer usar. O protótipo de uma função informa os atributos desta função para que o assembler (e o linker) possam fazer uma checagem dos tipos. O formato de um protótipo é o nome da função seguido da palavra-chave PROTO, e esta seguida da lista de parâmetros
formando pares de nome:tipo de dado separados por vírgulas. NomeDaFunção PROTO [NomeDoParâmetro]:TipoDeDado,[NomeDoParâmetro]:TipoDeDado...
Tendo essas informações podemos construir o protótipo, que nada mais faz do que definir ExitProcess como uma função que usa apenas um parâmetro do tipo DWORD: ExitProcess PROTO uExitCode:DWORD
Uau! Agora podemos mostrar o protótipo da ExitProcess ao construtor (assembler). É claro que o construtor precisa ser apresentado à função ANTES de fazer uso da mesma no código. A apresentação consiste no protótipo e na biblioteca que contém a função. Colocamos então este par junto com as outras solicitações: .386 .MODEL FLAT,STDCALL includelib \masm32\lib\kernel32.lib ExitProcess PROTO uExitCode:DWORD .CODE inicio: end inicio
Mas que negócio é este de includelib? A diretiva includelib é apenas uma maneira de indicar ao assembler quais as bibliotecas de importação que o programa usa. Quando o assembler encontra este tipo de diretiva, ele põe um comando para o linker no arquivo objeto para que o linker saiba quais bibliotecas de importação precisam ser vinculadas ao programa. É o caminho das pedras... Como eu disse na introdução, é muita teoria. Espero que, até este ponto, tudo tenha ficado claro para que possamos criar nosso fantástico programa que não faz nada Página 2 de 2
Criando o folgado Está tudo em riba? Se você seguiu o tutorial, o arquivo texto que você digitou no editor de texto do MASM deve estar com esta cara: .386 .MODEL FLAT,STDCALL includelib \masm32\lib\kernel32.lib ExitProcess PROTO uExitCode:DWORD .CODE inicio: invoke ExitProcess,0 end inicio
E isso é tudo. O arquivo texto está pronto. Agora o trabalho vai ser o de clicar no menu do editor do MASM:
Clique em [File / Save As] e salve o arquivo texto como folgado.asm Clique em [Project / Assemble ASM file] e o construtor do MASM irá produzir o arquivo folgado.obj Clique em [Project / Link OBJ File] e o linker do MASM irá produzir o arquivo folgado.exe
Se por acaso esquecermos o parâmetro da função (neste caso seria invoke ExitProcess ou invés de invoke ExitProcess,0), na etapa 2 o construtor (assembler) dará a seguinte mensagem de erro: Assembling E:\masm32\Projetos\Folgado\folgado.asm C:\masm32\Projetos\Folgado\folgado.asm (6) : error A2137 : too few arguments to INVOKE
Introduza o erro no texto e faça o teste só para ir pegando o jeito. E não se esqueça de SEMPRE salvar o arquivo texto antes de assemblar ou linkar. Se tudo correu bem após a estapa 2, o que significa que texto está sem erros que o construtor possa detectar, temos dois arquivos no diretório indicado: o folgado.asm, de 163 bytes, e o folgado.obj, de 463 bytes. E se tudo continuou a correr bem, após a etapa 3 tem mais um arquivo no diretório: folgado.exe, de 1.536 bytes. Enxutinho, não é mesmo? Tá certo que o programa não faz grande coisa, mas mesmo assim... Execute o folgado.exe e... surpresa! Tá pensando que eu vou dizer novamente que dá a impressão de que o programa não faz nada? Nananinanão! A surpresa é que o programa fez exatamente o que se esperava dele e que NÃO DEU ERRO!
Mais um pouco de teoria Folgado é o programa, nóis não! Só mais uma coisinha... Já pensou ficar montando protótipos de todas as funções que se queira importar de DLLs? É muito pra cabeça e vai acabar dando calo nos dedos (e detonando o teclado). Existe um modo muito mais elegante e folgado de obter o mesmo resultado: guardando os protótipos das funções em arquivos que possam ser incluídos no texto dos nossos programas. Na verdade, estes arquivos já estão prontos. Para cada biblioteca existe um arquivo de inclusão com todos os protótipos de todas as funções desta biblioteca. São os arquivos .inc. Para a kernel32.lib existe a kernel32.inc, para a user32.lib existe a user32.inc e assim por diante. A diretiva include é que faz a mágica. Basta usar include seguido pelo nome do arquivo que você quiser inserir no local onde se encontra a diretiva. Usando este expediente, o texto ficaria assim: .386 .MODEL FLAT,STDCALL includelib \masm32\lib\kernel32.lib include \masm32\include\kernel32.inc .CODE inicio: invoke ExitProcess,0 end inicio
Quando o MASM32 processa a linha "include \masm32\include\kernel32.inc", ele abre o arquivo kernel32.inc, que se encontra no diretório \masm32\include, e processa o conteúdo do arquivo como se o conteúdo do kernel32.inc estivesse presente no seu código. Desta
forma, pode-se substituir um caminhão de linhas com protótipos de funções com umas poucas linhas de include. Se você tiver curiosidade, abra num editor de texto um arquivo include qualquer. Estes arquivos estão em texto ASCII raso, o que significa que você pode lê-los com facilidade além de poder fazer seus próprios arquivos include e incorporá-los aos seus programas quando necessário. Os arquivos .inc podem conter outras definições (constantes, estruturas, etc) além de protótipos de funções. Um dos mais completos (e constantemente atualizado) é o arquivo windows.inc do Iczelion & Hutch. É pau pra toda obra. Você encontra este arquivo no pacote do MASM32 versão 7 do Hutch no site do Iczelion e no site do hutch. hutch.
Conferindo o trabalho do MASM Existem diversos desassembladores muito bons, como o WDasm e o ADA. Eu trabalho muito com o OllyDbg, um debugger gratuito para Windows com um desassemblador excelente. Você pode encontrá-lo no site do autor em emhttp://home.t-online.de/home/Ollydbg/ http://home.t-online.de/home/Ollydbg/.. Abrindo o executável com o OllyDbg encontramos o seguinte:
00401000 > $ 6A 00 PUSH 0 00401002 . E8 01000000 00401007 . CC INT3 00401008 $FF25 00204000 DS:[<&KERNEL32.ExitProcess> 0040100E 00 DB 00 0040100F 00 DB 00 ... ... 00401FFF 00 DB 00
; /ExitCode = 0 CALL ; \ExitProcess JMP DWORD PTR
No endereço 401000, ponto de entrada do executável (module entrypoint), já encontramos o push do parâmetro da função KERNEL32.ExitProcess. Logo depois vem o call para a função. O MASM trabalhou direitinho. Note que o nosso programa ocupa 4096 bytes (ou 4 Kb) de memória - vai do endereço 401000 até 401FFF - apesar de só ocupar 14 bytes com o código. O restante é preenchido com zeros.
Facilite a sua vida Para facilitar a sua vida coloquei um arquivo zipado com o tutorial e o código fonte na seção de downloads da Aldeia. Procure na categoria tutoriais/Assembly Numaboa.
Página 1 de 3
Se a sua plataforma é Windows, então você quer ver pelo menos uma janela. Neste tutorial vamos usar novamente a API do Windows para fazer a maior parte do trabalho. Como o SO é baseado em janelas, existem algumas delas prontinhas para serem usadas. A escolhida é a Caixa de Mensagem (Message Box), com a qual vamos colocar um recado na tela.
Projeto 1. 2. 3. 4.
O sistema operacional é o Windows de 32 bits. Não precisamos mais do que o conjunto de instruções do processador 386. O MASM vai fazer o trabalho pesado. O objetivo do programa é mostrar uma janela do tipo Message Box com um texto.
Botando a mão na massa Abra o QEditor do MASM32 e digite o esqueleto de um programa que possa ser finalizado (Exit Process). É idêntico ao programa do tutorial "O Folgado": .386 .MODEL FLAT,STDCALL includelib \masm32\lib\kernel32.lib include \masm32\include\kernel32.inc .CODE inicio: invoke ExitProcess,0 end inicio
Vamos novamente usar uma função da API do Windows mas, antes de analisá-la, é bom saber alguma coisa a mais sobre as funções. Existem dois tipos de funções na API: do tipo ANSI e do tipo Unicode. Os nomes das funções da API para ANSI possuem um sufixo "A", por exemplo, MessageBoxA. As para Unicode têm o sufixo "W" (acho que é para Wide Char). O Windows 9x/Me suporta ANSI e o Windows NT, o Unicode. As strings ANSI são as mais conhecidas. Elas são arrays de caracteres terminados em NULL. Um caractere ANSI tem o tamanho de 1 byte. Se o padrão ANSI é para 1 byte, isto significa que podem existir 256 caracteres diferentes (1 byte = 8 bits = 2ˆ8 = 256). Para as línguas
européias o código ANSI é suficiente, pois não existem mais do que 256 caracteres diferentes. Este padrão, no entanto, não é adequado para muitas das línguas orientais, que possuem alguns milhares de caracteres únicos. Este é o motivo da existência do UNICODE. Um caractere UNICODE tem o tamanho de 2 bytes, tornando possível a existência de 65.536 caracteres únicos nas strings.
A função MessageBox Na maioria das vezes usaremos um arquivo include que pode determinar e selecionar as funções da API apropriadas para a plataforma. Use apenas o nomes das funções API sem o sufixo que o MASM se encarrega do resto A função da API do Windows que "fabrica" uma janela do tipo Message Box é a MessageBox e faz parte da biblioteca user32.dll. Dando uma olhada na referência da API do Windows, encontramos o seguinte: int MessageBox( HWND hWnd, // manipulador da janela proprietária LPCTSTR lpText, // endereço do texto da message box LPCTSTR lpCaption, // endereço do título da message box
UINT uType
// estilo da message box
);
O valor de retorno da função é um inteiro e os parâmetros exigidos são:
hWnd: é o manipulador (handle) da janela-mãe. Você pode considerar o manipulador como um número que representa a janela. O valor deste número não é importante, apenas lembre-se de que representa uma janela. Quando você quiser fazer alguma coisa com a janela, refira-se a ela através do seu manipulador. lpText: é um ponteiro para o texto que se quer mostrar na área cliente da janela. Um ponteiro, na realidade, é o endereço de alguma coisa. Um ponteiro para uma string de texto é igual ao endereço de memória onde está esta a string. lpCaption: é um ponteiro para o título da janela. uType: especifica o ícone e o número e tipo de botões na janela message box.
Se conhecemos o nome da função e a biblioteca à qual ela pertence, além de informar o assembler/linker que esta função deve ser vinculada ao programa, podemos adicionar sem susto as linhas seguintes: .386 .MODEL FLAT,STDCALL includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib include \masm32\include\kernel32.inc include \masm32\include\user32.inc .CODE inicio: invoke ExitProcess,0 end inicio
Os parâmetros exigidos pela função MessageBox são:
hWnd é o manipulador da janela-mãe. Como não existe uma janela-mãe, porque nossa janela é a única, então este parâmetro pode ser NULL. lpText é o texto da janela - uma string ANSI terminada em zero. Lembre-se de que no Windows toda string ANSI precisa ser terminada em NULL (0 hexadecimal). Quem determina o texto que deve ser apresentado somos nós, portanto, vamos ter que inicializar esta string. lpCaption é o título da janela - mais uma string terminada em zero que precisa ser inicializada. uType determina o tipo da Message Box. Existem várias flags, agrupadas pelo tipo de aplicação, que determinam o conteúdo e o comportamento da MessageBox. Estas flags possuem nomes para facilitar o trabalho dos programadores. Assim, o grupo que determina os botões, possui flags como MB_OK (0 hexa), MB_YESNO (4 hexa), etc. O grupo do ícone possui MB_ICONWARNING (30 hexa), MB_ICONSTOP (10 hexa) e outros. Podemos fazer uma composição de flags para associar vários comportamentos e características fazendo um OR lógico com as flags desejadas. Por exemplo, para obter uma Message Box com botões Yes e No e com um ponto de interrogação como ícone, indica-se MB_YESNO (4 hexa) OR MB_ICONQUESTION (20 hexa).
Os parâmetros que precisam ser inicializados são o lpText e o lpCaption. A seção .DATA é onde estes dados podem ser inicializados. Os outros podem ser passados diretamente como valores ou como constantes predefinidas. Se quisermos utilizar as constantes predefinidas, é
preciso incluir um arquivo include que contenha as definições. É onde entra o excelente arquivo windows.inc do Iczelion & Hutch (está no pacote do MASM32 versões 8 e 9). O assembler faz distinção entre letras maiúsculas e minúsculas. Assim, nomejanela é diferente de NomeJanela. A sintaxe do MASM aceita uma diretiva que determina seu comportamento com letras maiúsculas e minúsculas: é a option casemap. Option casemap pode ser ALL, NONE e NOTPUBLIC (a última é o default). Usando option casemap:none logo abaixo da diretiva .MODEL, o MASM preserva a caixa dos identificadores. Usando option casemap:all, o MASM passa todos os identificadores para maiúsculo (por exemplo, tanto nomejanela quanto NomeJanela são transformados em NOMEJANELA). Queremos a ca ixa dos nossos identificadores preservada, então, para ir preparando o terreno, inclua o seguinte no script do seu programa: .386 .MODEL FLAT,STDCALL option casemap:none include \masm32\include\windows.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib include \masm32\include\kernel32.inc include \masm32\include\user32.inc .DATA TituloJanela db "Tutorial NumaBoa 3",0 TextoJanela db "Message Box NumaBoa",0 .CODE inicio: invoke ExitProcess,0 end inicio
Índice do Artigo Message Box (masm) Offset e Addr Conferindo Todas as páginas Página 2 de 3
offset e addr As variáveis TituloJanela e TextoJanela são as duas que precisavam ser inicializadas antes de se chamar a função MessageBox. De acordo com a referência da API, a função pede o PONTEIRO para estas variáveis e não o valor das mesmas. Para enviar um ponteiro de endereço como parâmetro usa-se tradicionalmente o operador offset . Se optarmos por chamar a função através de INVOKE, podemos utilizar o operador addr .
Os dois modos são válidos, mas você precisa conhecer a diferença entre eles:
addr não aceita referências posteriores, enquanto offset aceita. Por exemplo, se o
rótulo/variável for definido no código fonte adiante da linha que o invoca, addr não irá funcionar. invoke MessageBox, NULL, addr TextoJanela, addr TituloJanela, MB_OK ... TituloJanela db "Tutorial NumaBoa 3",0 TextoJanela db "Message Box NumaBoa",0
O MASM vai informar um erro. Se você usar offset ao invés de addr no trecho de código acima, o MASM vai assemblar numa boa.
addr pode lidar com variáveis locais, o offset só com variáveis globais. Uma
variável local é apenas algum espaço reservado na pilha. Você vai conhecer seu endereço apenas em tempo de execução. offset é interpretado pelo assembler em tempo de construção (assemblamento, assemblagem, assemblação, sei lá ) e aí fica claro porque offset não pode funcionar para variáveis locais. addr é capaz de lidar com variáveis locais devido ao fato do assembler primeiro checar se a variável referenciada por addr é global ou local. Se for uma variável global, ele põe o endereço dessa variável no arquivo objeto. Sob este aspecto, addr funciona como o offset. Se for uma variável local, ele gera uma sequência de instruções, antes da chamada à função, parecida com o seguinte: lea eax, LocalVar push eax
Uma vez que lea pode determinar o endereço do rótulo em tempo de execução, a coisa funciona bem. Então vamos lá. Após todo este trabalho de preparação, adicione a chamada à função na área de código: .386 .MODEL FLAT,STDCALL option casemap:none includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib include \masm32\include\kernel32.inc include \masm32\include\user32.inc include \masm32\include\windows.inc .DATA TituloJanela db "Tutorial NumaBoa 3",0 TextoJanela db "Message Box NumaBoa",0 .CODE inicio:
invoke MessageBox, NULL, addr TextoJanela, addr TituloJanela, MB_OK invoke ExitProcess,0 end inicio
Pelo exposto acima, você deve ter sacado que existem outras chamadas válidas. A tradicional seria fazer um push para a pilha dos parâmetros em ordem inversa (convenção C de passagem de parâmetros) e depois chamar a função com um call: push push push push call
MB_OK offset TituloJanela offset TextoJanela NULL MessageBox
Criando a Janela Se você optou pelo texto com a chamada de alto nível, o arquivo texto completo deve estar como o mostrado logo a seguir. Note que foi adicionado um ícone à janela, o MB_EXCLAMATION. Podemos enviar o parâmetro do tipo de janela como MB_OK OR MB_ICONEXCLAMATION ou como MB_OK+MB_ICONEXCLAMATION. O assembler aceita as duas versões. .386 .MODEL FLAT,STDCALL option casemap:none includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib include \masm32\include\kernel32.inc include \masm32\include\user32.inc include \masm32\include\windows.inc .DATA TituloJanela db "Tutorial NumaBoa 3",0 TextoJanela db "Message Box NumaBoa",0 .CODE inicio: invoke MessageBox, NULL, addr TextoJanela, addr TituloJanela, MB_OK+MB_ICONEXCLAMATION invoke ExitProcess,0 end inicio
Agora é só produzir o executável. No tutorial anterior, "O Folgado", vimos como assemblar e linkar o arquivo texto usando as opções correspondentes do menu do MASM. Desta vez, vamos fazer o trabalho na unha: 1. Salve o exemplo com um nome qualquer, por exemplo, msgbox.asm 2. Transforme o arquivo texto em arquivo objeto usando o construtor do MASM chamado ML.EXE. Este programa fica no diretório \masm32\bin. Se este diretório estiver no path, use a seguinte linha de comando: ml /c /coff /Cp
msgbox.asm o /c indica ao MASM para apenas assemblar, ou seja, ele não vai chamar o link.exe automaticamente. É conveniente rodar apenas o construtor para verificar se há algum erro no arquivo texto. Se tudo estiver correto, chamamos o link.exe posteriormente. o /coff indica ao MASM para criar o arquivo .obj no formato COFF. O MASM usa uma variação do COFF (Common Object File Format) que é usado em sistemas UNIX como formato próprio de arquivos objeto e executáveis. o /Cp indica ao MASM para preservar maiúsculas e minúsculas dos identificadores usados. Se você incluir \masm32\include\windows.inc, então pode colocar "option casemap:none" no cabeçalho do código fonte, logo abaixo da diretiva .model, para obter o mesmo resultado. 3. Se não houve mensagens de erro, você pode transformar o arquivo objeto msgbox.obj em executável. Use o linker do MASM, o LINKER.EXE, que também se encontra no diretório \masm32\bin, com a seguinte linha de comando: link /SUBSYSTEM:WINDOWS/LIBPATH:c\masm32\lib msgbox.obj o /SUBSYSTEM:WINDOWS informa ao linker o tipo de executável que deve ser criado. o /LIBPATH: informa a localização das bibliotecas de importação. No MASM32, elas estarão no diretório masm32\lib. Se tudo correu bem, agora você é o feliz possuidor de um executável denominado msgbox.exe, de apenas 2.560 bytes, cuja função é mostrar esta janelinha:
Message Box Numaboa
Índice do Artigo Message Box (masm) Offset e Addr Conferindo Todas as páginas Página 3 de 3
Conferindo o trabalho do MASM Usando novamente o OllyDbg como desassemblador, encontramos o seguinte: 00401000 > $ 6A 00 PUSH 30 MB_OK|MB_ICONEXCLAMATION|MB_APPLMODAL 00401002 . 68 00304000 "Tutorial NumaBoa 3" 00401007 . 68 13304000 "Message Box NumaBoa" 0040100C . 6A 00 PUSH 0 0040100E . E8 0D000000 00401013 . 6A 00 PUSH 0 00401015 . E8 00000000 0040101A $FF25 00204000 DS:[<&KERNEL32.ExitProcess> 00401020 $FF25 08204000 DS:[<&USER32.MessageBoxA>]
; /Style = PUSH NUMABOAB.00403000
; |Title =
PUSH NUMABOAB.00403013
; |Text =
; |hOwner = NULL CALL ; \MessageBoxA ; /ExitCode = 0 CALL ; \ExitProcess JMP DWORD PTR JMP DWORD PTR
No endereço 401000, ponto de entrada do executável (module entrypoint), encontramos todos os push dos parâmetros da função USER32.MessageBoxA. Logo depois vem o call para a função. Somente quando retornamos da função MessageBoxA é que caímos no ExitProcess, que encerra nosso programa. Observe que nos endereços 401002 e 401007 está sendo feito o push dos ponteiros para as strings. Verificando a área de memória indicada pelos ponteiros, o OllyDbg mostra as strings terminadas em zero: 00403000 54 75 74 6F 72 69 61 6C 20 4E 75 6D 61 42 6F 61 NumaBoa 00403010 20 33 00 4D 65 73 73 61 67 65 20 42 6F 78 20 4E Box N 00403020 75 6D 61 42 6F 61 00 00 00 00 00 00 00 00 00 00 umaBoa..........
Tutorial 3.Message
Os palpites da vó
Já que você está com o MASM aberto e o código fonte na sua frente, dê uma boa estudada na referência da função MessageBox. Brinque com as flags e ponha mais botões, troque de ícone, defina o botão ativo, mude o alinhamento do texto, etc. Aproveite e dê uma olhada na função MessageBoxEx. Se você tiver interesse nas funções da user32.lib e não tiver uma referência da API, baixe o arquivo de Referências da API Win32 na seção de downloads da Aldeia (procure em Informática/Referências). O arquivo zipado com este tutorial e seu código fonte está na seção de downloads da Aldeia. Procure na categoria tutoriais/Assembly Numaboa.
No tutorial anterior fizemos uma caixa de mensagem (Message Box) usando o editor que acompanha o MASM32. O qEditor é um tanto encrencado para usar, por isto, sugiro que os interessados experimentem o RadASM. O RadASM é uma interface gráfica que facilita a programação. Seu autor, Ketil0, há anos faz atualizações no software que, desde sempre, foi disponibilizado gratuitamente para a comunidade do assembly.
Instalador RVLCN
Você pode fazer o download do RadASM versão 2.2.1.6, juntamente com o pacote de Assembly e o pacote de idiomasaqui na Aldeia, mas sugiro que dêem uma procurada no Google para ver se há versões mais recentes porque o site do Ketil0 infelizmente sumiu Há alguns dias atrás publiquei uns tutoriais escritos pelo Alan Moreno e fiquei sabendo que ele disponibiliza um pacote com MASM32 enxutinho com a opção de também instalar e configurar o RadASM. É uma mão na roda porque não é preciso fazer a mexida toda quando instalamos cada um em separado. Para quem quiser fazer tudo na unha, o Cap. I - Introdução explica direitinho tudo o que é preciso fazer para instalar e, principalmente, para configurar o MASM32+RadASM para que tudo funcione de acordo. Para os mais folgados (assim como eu), basta fazer odownload do instalador do Alan e pregar o chinelo. Rode o instalador, marque a caixa "Instalar RadASM v2.2.0.9" e clique no botão "Instalar". A versão do RadASM é um pouco mais antiga do que a disponibilizada pelo Ketil0, mas não tem problema. Ao invés de repetir todo o Message Box adaptando-o para o Radasm, desta vez fiz uma apresentação que explica como tirar o máximo desta ferramenta. Faça o download da apresentação (em Downloads / Tutoriais / Assembly Numaboa), que é um executável, para acompanhar todos os passos. A todos, bom divertimento e bom estudo.
Últim
Quando se pretende programar em Assembly é essecial conhecer o processador e o sistema operacional para os quais o executável se destina. Neste texto abordaremos apenas o sistema operacional Windows. O Windows assume o controle do computador praticamente desde o instante em que é ligado até o momento em que é desligado. Uma aplicação pode rodar apenas com a permissão do Windows, com a assitência do Windows e sob o controle do Windows. É deste modo que o Windows oferece a previsibilidade e a consistência da interface com o usuário e é por este motivo que possui a capacidade de (aparentemente) rodar diversos programas simultaneamente (a chamada multi-tarefa). Este sistema opercaional deveria oferecer maior robustez em casos de falhas, mas isto é um capítulo à parte Para resumir a história, o sistema operacional Windows domina a sua máquina, controlando tudo o que for possível: Hardware, Aplicativos, Microprocessador, Memória e Dados, Interface do usuário e Arquivos.
Controle do hardware A maioria dos microchips que trabalham com a unidade central de processamento (CPU) são programáveis. Por exemplo, a placa de vídeo precisa ser informada do scan rate, resolução e cores corretos. Os chips de entrada/saída da impressora precisam conhecer a porta normalmente utilizada e a que velocidade os dados devem ser transferidos. Os chips do teclado devem conhecer a taxa de repetição que devem usar. A comunicação com estes dispositivos precisa ser controlada – cada um terá sua própria área de memória e precisa ser informado do que se espera e quando. O Windows realiza todas estas tarefas básicas, como qualquer outro sistema operacional. O Windows também assume o controle total da escrita e da leitura destes dispositivos, o que pode ser uma grande vantagem para o programador. Por exemplo, para imprimir um documento, o aplicativo precisa apenas informar em que parte da memória o documento se encontra e qual o seu tamanho. O Windows se encarrega da impressão usando o driver de impressão adequado para a impressora em uso e coloca a tarefa na posição correta da fila de impressão, que pode conter outras tarefas de impressão de outros
aplicativos. A tarefa de impressão é sempre efetuada no modo gráfico. O Windows informa a impressora onde cada ponto de impressão deve ser colocado no papel para formar a imagem impressa. As vantagens do programador do aplicativo são significantes - não há a necessidade de escrever drivers de impressão nem algoritmos gráficos. O Windows faz um trabalho ainda melhor na tela do monitor, que também sempre está no modo gráfico. Ele atende vários aplicativos simultaneamente e é capaz de colocar diversas janelas na tela, algumas delas sobrepostas. Sua tarefa é criar a imagem final e de gerenciar cada uma das janelas respeitando a prioridade, a ordem na tela, tipo e estilo de cada uma delas. Uma boa parte do trabalho de programação consiste em aplicar estes fatores ao programa e obter uma saída correta para a tela. Em resumo: o Windows controla todo o hardware dos periféricos e impede (ou deveria impedir) o acesso direto a eles através de aplicativos.
Controle dos aplicativos O que acontece quando se clica o ícone de um programa? O Windows sabe exatamente a posição do clique e qual o ícone que se encontra sob o cursor do mouse. Também sabe, através da sua lista de "atalhos" e "propriedades", qual programa deve ser iniciado quando este ícone em particular for clicado. Para iniciar o programa, o Windows carrega o programa lendo o arquivo correspondente e colocando-o na memória. Depois, o Windows simplesmente chama este programa, ou seja, informa o processador para que ele execute todas as instruções a partir do endereço inicial do programa até encontrar uma instrução RET. Após o RET, termina o programa e volta ao sistema operacional.
Controle do microprocessador Ao iniciar um programa, o registrador EIP do processador recebe do Windows o endereço inicial do programa. O Windows também controla todos os valores dos registradores do processador, mantendo-os numa área de memória chamada de contexto de registradores. O Windows pode (e o faz com frequência) parar o processador, armazenar os valores contidos nos registradores e solicitar que o processador rode um programa diferente por algum tempo, ou seja, fornece a outro programa uma fatia de tempo. Terminando o segundo programa, o Windows reconstitui os valores armazenados e continua executando o primeiro programa a partir do ponto de interrupção. É desta forma que funciona a multi-tarefa do Windows. Cada um dos programas que estiver sendo executado recebe uma fatia de tempo - o processador é rateado entre todos eles e, mesmo em máquinas com apenas uma CPU, o usuário tem a impressão de que os programas são executados simultaneamente. O Windows faz o rateio de tempo de acordo com várias prioridades. Por exemplo, operações de leitura e escrita em disco possuem prioridades muito altas e podem bloquear a execução de outros programas até que sejam finalizadas.
Um programa pode pedir ao Windows para que inicie uma nova linha de execução ( thread). Neste caso, o Windows atribuirá a este thread fatias de tempo próprias, valores de registradores próprios e um pilha própria. A nova linha de execução parece estar sendo executada ao mesmo tempo que o thread principal do programa. Isto é muito útil quando um programa precisar dar continuidade a uma determinada tarefa, por exemplo um cálculo muito longo, e, ao mesmo tempo, manter a interface do usuário ativa. Isto é chamado de multi-threading.
Controle de memória e dados Num dado momento, um programa pode ter todos os seus dados na memória. Estes são mantidos na memória que foi estabelecida por endereçamento direto ou então na pilha. Como o Windows preserva estes dados quando ratear o tempo entre vários programas? O Windows mantém um mapa de memória de todos os dados de programas, ou seja, ele conhece o local exato dos dados dos programas na memória física do computador. Este mapa de memória é mantido numa área de memória chamada de contexto de memória. Se a memória física começar a se esgotar, o Windows passa a usar o disco rígido para armazenar os dados dos programas. Isto explica porque, em sistemas com pouca memória, a atividade do HD é muito maior do que em sistemas com mais memória física. Esta memória em disco é chamada de memória virtual. Quando um programa precisa acessar seus dados, ele o faz usando um endereço virtual. Isto significa que o endereço da área de memória na verdade não é o mesmo que o endereço dos dados na memória física. O Windows informa o processador onde as áreas de memória requeridas se encontram realmente fornecendo o endereço da sua tabela de mapeamento de páginas ao programa através do registrador CR3.
Controle da interface do usuário O Windows possui uma interface de usuário consistente, largamente difundida e utilizada. As vantagens de uma interface padronizada são óbvias - uma delas é que, independentemente do programa que esteja sendo executado, o usuário se sente "em casa". O Windows adquiriu esta uniformidade por que fornece componentes padrão que podem ser incluídos nos programas. Exemplos disto são os menus que aparecem sob a barra de título das janelas, diálogos, botões, barras de rolagem e arquivos de ajuda padronizados. O conjunto destes componentes padrão é denominado de GUI ou Graphical User Interface. Os aplicativos podem fazer uso destes componentes padrão fazendo uma chamada a uma API (Applications Programming Interface). As APIs contêm procedimentos (ou funções) que podem ser chamados por um nome e que fornecem o componente desejado. Todas as APIs são armazenadas em arquivos chamados DLL ou Dynamic Linked Library. Na verdade, as DLLs são executáveis com a extensão .dll que contêm funções nominadas que podem ser exportadas (ou seja, um aplicativo importa funções).
Controle de arquivos O Windows mantém registros de todos os arquivos vitais ao sistema e de drivers de dispositivos periféricos. Para isto usa o Registro (registry), uma base de dados com informações sobre a configuração do sistema e dos aplicativos que devem rodar neste sistema. Além disto, mantém o registro de todas as pastas (diretórios) com seus respectivos conteúdos para que possam ser localizados quando solicitados.
A comunicação sistema-aplicativo Como vimos, o sistema Windows controla todos os aspectos importantes do computador e dos aplicativos que estejam rodando. Para que este controle cerrado possa ser mantido, é necessário haver um sistema de comunicação entre o sistema e os aplicativos. Um aplicativo tem necessidade de se comunicar com o sistema quando quiser obter alguma informação da GUI, por exemplo, o tamanho de uma janela em particular ou o tamanho de uma string numa determinada fonte. O mesmo acontece quando o aplicativo necessitar de algum recurso da API, pois precisa informar com exatidão como este recurso deve ser aplicado. Os métodos mais comuns usados por aplicativos para se comunicarem com o sistema são:
Dados na pilha: antes de fazer uma chamada a uma API, é necessário colocar na pilha, utilizando PUSH, os dados exigidos pela função da API. Os dados, na maioria das vezes, são valores dword. Em alguns casos podem ser ponteiros de estruturas que contenham mais dados ou ponteiros de strings de texto. Mensagens: pode-se enviar mensagens ao sistema chamando a função SendMessage da API. Na realidade, a mensagem é um dword colocado na pilha e que pode ser acompanhado por até 3 dwrods de dados adicionais.
O sistema também precisa se comunicar com o aplicativo para fornecer o resultado de uma chamada à API ou para informar o aplicativo de que algo está acontecendo na GUI ou que algo importante está ocorrendo com o próprio sistema. Os métodos mais comuns de comunicação entre o sistema e um aplicativo são:
Retornando de uma API, o sistema geralmente põe um valor que representa o resultado da chamada no registrador EAX. Em alguns casos, retornando de uma API, o sistema deixa dados na memória, num local especificado pelo programa quando fez a chamada à API. Este local deve ter sido especificado pelo aplicativo PUSHando um ponteiro para a pilha antes da chamada à API. Mensagens do sistema para o aplicativo. Quando isto ocorre, o sistema também envia dados para a pilha. O sistema precisa ter sido avisado pelo aplicativo do endereço no seu código que corresponde ao procedimento que gerencia este tipo de chamada. Este procedimento é chamado de procedimento "callback"
(chamada de retorno), "windows procedure" (procedimento windows) ou simplesmente "WndProc".
Manipuladores e Contextos de dispositivo Todos os "objetos" com os quais o Windows trabalha possuem manipuladores (handles). Estes objetos podem ser janelas, controles, menus, diálogos, processos, threads, áreas de memória, displays, impressoras, arquivos, drives de disco e até fontes, brushes e pens usados para desenhar e escrever. Um manipulador é um valor dword que pode ser requisitado pelo aplicativo. Uma vez obtido, este manipulador é utilizado pelo aplicativo para se comunicar com o Windows e solicitar seu uso ou modificações. Todos os dispositivos que mostram ou produzem uma saída possuem contextos de dispositivo. O contexto de dispositivo é uma área de memória, mantida pelo Windows, que contém informação sobre como o dispositivo deve mostrar sua saída. Portanto, uma janela em particular terá um contexto de dispositivo que conterá informações sobre a fonte e a cor que devem ser usadas para qualquer coisa que for desenhada ou escrita nesta janela. Uma impressora terá um contexto de dispositivo contendo as características da impressora, tamanho do papel, cores disponíveis e assim por diante.
Tipos de executáveis Um "executável" é um arquivo que contém código que pode ser executado pelo processador. Como iniciante, só é preciso conhecer dois tipos: os arquivos com extensão .exe (aplicativos) e os arquivos com extensão .dll (dynamic link library ou blibliotecas dinâmicas). Para poder ser executado pelo Windows, um arquivo executável precisa estar no formato PE (Portable Executable). Como o nome sugere, este tipo de arquivo possui portabilidade, o que permite pode seja executado em computadores tanto com processadores Intel, MIPS, Alpha, Power PC, Motorola 68000, assim como RISC. É claro que, independentemente do tipo de processador, o sistema operacional precisa ser Windows e o versão do formato PE precisa corresponder ao processador utilizado. O Windows sabe que o executável é um arquivo PE devido à presença da assinatura "PE" logo no início do arquivo. Um arquivo não-PE, por exemplo um executável DOS, não possui esta assinatura e o Windows precisa tomar outras providências para rodá-lo. Uma Dll é usada quando seu código ou seus dados precisam ser compartilhados entre diversos aplicativos. O Windows usa Dlls para armazenar o código da sua API. Uma Dll possui exportações. Isto reduz sensivelmente o tamanho de cada exe porque, quando precisar do código, recorre a uma Dll. Um campo de grande importância num arquivo PE é a lista de importação. Esta é uma lista das funções das quais o Exe depende e que poderá chamar quando estiver sendo executado. Esta lista também possui o nome da Dll que contém a função. Ao carregar o Exe, o Windows checa se todas as funções e todas as Dlls estão disponíveis. Se não estiverem disponíveis, o sistema não roda o programa. Diferentes versões do Windows possuem Dlls diferentes. Fica claro que, com toda a probabilidade, um programa escrito para rodar em Win98 acabe não rodando no
WinNT. Para evitar este problema pode-se utilizar a função da API GetVersionEx, a qual determina a versão atual do Windows, e, de posse desta informação, chamar a API correta. Só que há um detalhe: se a API for chamada do modo usual, a Dll entra na lista de importação e, se não estiver presente, o arquivo não é executado. Contorna-se este problema utilizando a função da API LoadLibrary (que carrega a Dll desejada se já não tiver sido carregada) e GetProcAddress (que acha o endereço na Dll da função desejada). Assim como o Windows usa Dlls, também é possível escrever Dlls que acompanhem um aplicativo composto de mais de um programa. Desta forma os programas podem compartilhar o código e os dados existentes na Dll. Existe mais um caso no qual o uso de Dlls pode ser vantajoso: coloca-se código e dados que precisam de manutenção frequente numa Dll. Ao invés de ter que atualizar todo o arquivo exe, trabalha-se com um arquivo menor e "segregado", o que facilita a manutenção.
Todos programas fazem uso intensivo da pilha em tempo de execução. Quando se programa usando uma linguagem de alto nível, este aspecto passa batido e a gente nem toma conhecimento do assunto. Um programador assembly, no entanto, precisa ficar esperto porque a pilha é uma das ferramentas mais importantes que ele tem à sua disposição. Saber trabalhar com a pilha é uma enorme vantagem, apesar de não ser indispensável. Em todo caso, sempre é bom ter uma noçãozinha da coisa.
Características e vantagens da pilha A pilha é basicamente uma área de dwords (área de dados de 32 bits) existente na memória em tempo de execução, na qual o aplicativo pode armazenar dados temporariamente. Possui certas características e vantagens reais em relação a outros tipos de armazenamento na memória (seção de dados e áreas de memória em tempo de execução). São elas:
O processador é muito veloz no acesso à pilha, tanto para escrever quanto para ler, por que é otimizado para esta tarefa. As instruções muito simples de PUSH e POP podem ser usadas para escrever e ler na pilha. Estas instruções são muito compactas, possuindo apenas um byte quando usam registradores ou cinco bytes quando usam marcadores (labels) de memória ou ponteiros para endereços de memória. No Windows, a pilha é ampliada em blocos de 4Kb em tempo de execução. Isto evita desperdício de memória.
A pilha pode ser usada para:
Preservar valores de registradores em funções (exemplo) Preservar dados da memória (exemplo) Transferir dados sem usar registradores (exemplo) Reverter a ordem de dados (exemplo)
Chamar outras funções e depois retornar (exemplo) Passar parâmetros para funções (exemplo)
Registrador ESP , o ponteiro da pilha O registrador ESP (acrônimo de "extended stack pointer") contém o topo da pilha. Este é o ponto usado pelas instruções que utilizam a pilha (PUSH, POP, CALL e RET). Adiante falaremos mais sobre o assunto. Normalmente o programador faz o registrador EBP (acrônimo de "extended base pointer") apontar para um determinado lugar da pilha para que seus dados possam ser lidos ou escritos usando um endereçamento com base indexada. Por exemplo, na instrução MOV EAX,[EBP+8h], o registrador EBP é usado como um índice para uma área da pilha e esta instrução irá transferir da pilha para o registrador EAX um dword situado 8 bytes adiante. A origem do uso do registrador EBP associado à pilha é da época dos sistemas de 16 bits, que tinham toda aquela complicação com segmentos e outros que tais. Nos sistemas de 32 bits não é necessário manter esta associação e o registrador EBP pode ser utilizado como um registrador de uso geral. Apenas por hábito ele continua sendo usado para endereçar determinadas áreas da pilha, principalmente para acessar parâmetros passados para funções e rotinas de callback e para endereçar dados locais.
Armazenando e retirando dados da pilha A pilha pode ser imaginada como uma pilha de pratos. Isto funciona na base de "último a entrar, primeiro a sair". O último prato colocado na pilha usando uma instrução PUSH será o primeiro a ser retirado com uma instrução POP (se não for assim, a pilha cai ). O ponteiro da pilha em ESP sempre aponta para este prato no topo. Voltando ao computador. Suponha que o valor de ESP seja 64FE3Ch e que você tenha as seguintes instruções no seu código fonte: PUSH 2 PUSH [hWnd] PUSH ADDR STRING
Após estas três instruções, ESP estaria com o valor 64FE30h (12 bytes ou 3 dwords a menos) e a pilha teria o seguinte aspecto: ESP está aqui ->
64FE30h 64FE34h 64FE38h 64FE3Ch
endereço de STRING valor de hWnd número 2
Observe que cada instrução PUSH diminui o valor de ESP em 4 bytes. Observe também que, uma vez que ESP aponta para o último dword PUSHado para a pilha, o próximo PUSH vai escrever em ESP-4h. Isto é feito pelo
processador, que reduz o ESP em quatro e depois escreve o dword no endereço que ESP contém. Agora vamos ver como se comporta o POP. Usando os mesmos valores da pilha, usaremos as seguintes instruções: POP EAX POP EBX POP ECX
Despois destas três instruções a pilha terá o seguinte aspecto:
ESP está aqui ->
64FE30h 64FE34h 64FE38h 64FE3Ch
endereço de STRING -> EAX valor de hWnd -> EBX número 2 -> ECX
A primeira coisa a ser observada é que, após estas três instruções, o ESP está de volta em 64FE3Ch. Isto significa que o equilíbrio de ESP foi restaurado. Este é um conceito muito importante (veja logo abaixo). O registrador EAX agora contém o endereço de STRING, o EBX contém o valor de hWnd e o ECX contém o número 2. Percebe-se que os dados armazenados na pilha foram retirados pelo POP na ordem inversa em que foram colocados. Observe também que os dados da pilha continuam presentes! Isto acontece por que a instrução POP não escreve na pilha. Ela apenas lê os dados da pilha e os transfere para a segunda parte da instrução (chamada de "operando").
Preservando valores de registradores em funções Programas escritos em Assembly são rápidos porque usam os registradores exaustivamente, só que isto muitas vezes exige que os valores dos registradores sejam preservados para uso futuro. Por exemplo, imagine que um manipulador de arquivo (handle) esteja em EDI e que, após alguns cálculos com a ajuda do EDI, você tenha que fechar o manipulador. Para preservá-lo pode-se fazer o seguinte: PUSH EDI CALL CALCULA POP EDI CALL CLOSE_FILEHANDLE
;salva o manipulador de arquivo ;faz alguns cálculos (usando EDI) ;recupera o manipulador de arquivo ;fecha o manipulador contido em EDI
Uma outra alternativa é preservar o EDI dentro do procedimento CALCULA: CALL CALCULA CALL CLOSE_FILEHANDLE CALCULA: PUSH EDI . .
;faz alguns cálculos (salva EDI) ;fecha o manipulador contido em EDI
;salva o manipulador de arquivo ;código usando EDI
. POP EDI RET
;recupera o manipulador de arquivo
Outra razão para um registrador ser preservado é quando uma função em particular é chamada externamente (por outra função no mesmo programa, por outro programa ou pelo sistema). Na maioria dos casos deve-se garantir que EBP, EBX, EDI e ESI sejam preservados. Programas em C ou Delphi que chamam rotinas em Assembly e procedimentos callback chamados pelo próprio Windows com certeza exigem esta preservação. Um exemplo de procedimento callback é um procedimento de uma janela que é usada pelo sistema para passar informações para uma janela de um aplicativo. Nestas circunstâncias é necessário garantir os valores dos registradores usando, por exemplo: PUSH EBP,EBX,EDI,ESI . . . POP ESI,EDI,EBX,EBP
;seu código vai aqui
É óbvio que, se estes registradores não forem modificados pelo código, alguns PUSH e POP não são necessários. Mesmo assim, é uma boa prática garantir a preservação dos seus valores - o seguro morreu de velho. Note que os POP estão na ordem inversa dos PUSH - isto é para respeitar o "último a entrar, primeiro a sair" da pilha. Observe também que os registradores estão em ordem alfabética. É um pequeno truque para não esquecer nenhum deles. Caso você esteja trabalhando com o GoAsm, a declaração USES preserva e restaura automaticamente todos os registradores.
Preservando dados da memória Da mesma forma que é possível preservadar valores de registradores usando a pilha, pode-se também preservar dados da memória. Suponha que você tenha calculado cuidadosamente o número de widgets e quer escrever os detalhes dos widgets na tela além de gravá-los em arquivo. Você pode usar o seguinte código: PUSH [NRODE_WIDGETS] ;guardar número de widgets L2: CALL REPORT_WIDGET ;escrever detalhes do widget na tela DEC D[NRODE_WIDGETS] ;decrementar o número de widgets ;continuar com o próximo enquanto não for zero JNZ L2 ;restaurar o número de widgets POP [NRODE_WIDGETS] CALL WRITETO_FILE ;e gravar em arquivo
Transferindo dados sem usar registradores Suponha que você queira transferir o número de widgets para um outro marcador
(label) de memória. Você poderia usar: MOV EAX,[NRODE_WIDGETS] MOV [COPIADE_NRODE_WIDGETS],EAX
Igualmente eficiente seria: PUSH [NRODE_WIDGETS] POP [COPIADE_NRODE_WIDGETS]
Como esta segunda opção não faz uso do registrador EAX, este registrador não perderia seu valor e poderia ser utilizado para outra finalidade.
Revertendo a ordem de dados Você pode tirar vantagem da característica "último a entrar, primeiro a sair" da pilha para inverter a ordem de dados. Um exemplo muito prático é escrever na tela um valor decimal. Neste exemplo, EAX contém o valor que deve ser escrito e EDI contém a posição de memória do buffer que abrigará a string com os algarismos: XOR EDX,EDX XOR ECX,ECX MOV EBX,10 L2: DIV EBX edx PUSH EDX INC ECX XOR EDX,EDX CMP EAX,EDX JNZ L2 L3: POP EAX ADD AL,48 STOSB LOOP L3
;zera edx ;zera ecx (usado como contador) ;ebx guarda sempre o valor 10 ;divide edx:eax por 10 - quociente em eax, resto em ;põe resultado na pilha ;conta quantos foram feitos ;zera edx ;vê se há mais para ser feito ;sim ;agora reverter a ordem dos dígitos ;pega o próximo da pilha ;converte para número ascii ;escreve número ascii no buffer ;continua enquanto ecx for diferente de zero
Vamos analisar este código. Imagine que o valor em EAX seja 123 decimal. A primeira divisão por dez põe 12 em EAX e 3 em EDX. 3 é colocado na pilha. A segunda divisão por dez põe 1 em EAX e 2 em EDX. 2 é colocado na pilha. A terceira divisão por dez põe zero em EAX e 1 em EDX. 1 é colocado na pilha. O resultado de CMP EAX,EDX então é zero e a execução do código é desviada para o marcador L3. ECX está com 3 porque contou o número de dígitos. Agora cada um deles é retirado da pilha e adicionado a 48. Para 1, 2 e 3 obtemos respectivamente 49, 50 e 51. Estes valores são transferidos para o buffer e correspondem aos caracteres ascii "1", "2", e "3". Como foram colocados na pilha na ordem inversa (321) e foram retirados novamente na ordem inversa (123), já estão na sequência desejada e prontos para, mais tarde, serem escritos na tela.
Como CALL e RET usam a pilha A instrução CALL é muito usada em programação. É utilizada para desviar a execução para um procedimento (ou "função") em particular. Quando o procedimento termina, a execução continua logo após a linha da chamada. Chamando procedimentos ajuda a manter o código fonte limpo e mais fácil de entender. Por exemplo: 401020: MOV EAX,EDX 401022: CALL CALCULA_CUSTOS 401027: MOV [CUSTOS],EAX
;põe resultado da chamada na memória
Não há dúvida de que o procedimento CALCULA_CUSTOS deve realizar um trabalho extenso, porém, neste ponto do código, não há a necessidade de se preocupar com isso. Usando calls também ajuda a manter a modularidade do código, ou seja, o procedimento CALCULA_CUSTOS também pode ser usado por outros programas. Se quiser, pode considerá-lo como um "objeto". A programação orientada a objeto é basicamente isto. Como é que o processador sabe onde continuar o processamento depois de uma chamada? Muito simples: ele coloca o endereço de retorno na pilha! Vamos dar uma olhada na pilha no momento em que acontece uma chamada. Imagine que o valor de ESP seja 64FE3Ch e que o código fonte seja o mostrado acima. Após a primeira instrução, é claro que ESP ainda está em 64FE3Ch e a pilha não foi modificada por que ela não é afetada pela instrução MOV. Mas, quando a instrução CALL CALCULA_CUSTOS é executada, o processador PUSHa para a pilha o endereço de retorno 401027h. Bem, no procedimento CALCULA_CUSTOS existe uma instrução RET (retornar ao chamador), por exemplo: CALCULA_CUSTOS: RET
;um monte de código aqui ;retornar ao chamador
A instrução RET causa um POP para EIP. Em outras palavras, seja o que for que estiver em [ESP] é atribuído a EIP (o ponteiro de instruções) e depois ESP (o ponteiro da pilha) é incrementado em 4 bytes. Vamos observar o que acontece com a pilha antes, durante e depois destas instruções. Note como o equilíbrio do ESP é restaurado: Antes da Chamada 64FE30h 64FE34h 64FE38h ESP -> 64FE3Ch
Durante a Chamada 64FE30h 64FE34h ESP -> 64FE38h 401027h 64FE3Ch
Depois da Chamada 64FE30h 64FE34h 64FE38h 401027h ESP -> 64FE3Ch
A importância do equilíbrio da pilha Vimos como um procedimento pode ser chamado e o endereço de retorno é mantido na pilha. Acontece que, com frequência, procedimentos chamam outros procedimentos que chamam outros procedimentos... e assim por diante. Podemos ter, por exemplo: CALCULA_CUSTOS: CALL CALCULA_CUSTOFIXO RET
;retorna ao chamador
CALCULA_CUSTOFIXO: ;uma porção de código
CALL GET_CUSTOVARIAVEL CALL AJUSTEPARA_DEPRECIACAO ;tira o equilíbrio de ESP ADD ESP,4 RET
Neste exemplo, a tarefa é dividida em vários componentes. Imagine que o procedimento CALCULA_CUSTOFIXO adicione 4 a ESP por engano. Se isto acontecer, quando a instrução RET for executada, o ponteiro de instruções EIP estará carregado com um valor errado e o programa vai dar pau. Enquanto um procedimento estiver sendo executado é comum que o ESP seja deslocado (por exemplo, quando é preciso abrir um espaço na pilha), mas é de vital importância assegurar que o equilíbrio da pilha seja restaurado assim que o procedimento chegar no fim. O equilíbrio da pilha também é importante ao retornar para o Windows, mesmo num programinha minúsculo. O aplicativo Windows mais simples possível, que não faz absolutamente nada, é o seguinte: START: RET
onde START é a entrada do aplicativo. Na realidade, o Windows normalmente chama seu aplicativo através da Kernel32.dll, de modo que um simples RET termina o programa alegremente sem maiores problemas por que esta e outras DLLs da API cuidam que a pilha se mantenha equilibrada.
Usando a pilha para passar parâmetros As APIs do Windows esperam receber parâmetros através da pilha. Portanto, quando chamamos uma API, é necessário PUSHar os parâmetros necessários para que estes possam ser resgatados pela API. PUSH 1,[hButton]
CALL EnableWindow
;habilitar botão
Inicialmente colocamos o valor 1 (flag ENABLE, habilitar) na pilha, seguido pelo manipulador da janela que quermos habilitar. O Windows usa a convenção de chamada padrão "C" para suas APIs de modo que, ao retornar da API, a pilha estará novamente em equilíbrio. A convenção também significa que EBP, EBX, ESI e EDI sempre são restaurados pela API. Outro aspecto da convenção é que os parâmetros são sempre PUSHados da direita para a esquerda (ou do último para o primeiro). As especificações para a função EnableWindow no Windows Software Development Kit são: WINAPI EnableWindow( HWND hWnd, BOOL bEnable );
Para traduzir para Assembly, é preciso ler do fim para o começo. A coisa fica um pouco mais fácil se usarmos a instrução INVOKE ao invés de CALL. Neste caso, a ordem dos parâmetros é a mesma do SDK: INVOKE EnableWindow, [hButton], 1
Finalmentes UFA!!! Foi muito pra cabeça? Espero que não. Entender o funcionamento da pilha, a meu ver, é essencial para produzir programas de qualidade. Não posso deixar de agradecer Jeremy Gordon pelos seus excelentes artigos sobre a pilha. O presente texto é (praticamente) apenas a tradução de Understand the stack (part 1) do referido autor.
Este tutorial trata de aspectos mais avançados da pilha. O texto base para este assunto você encontra no tutorial Assembly e o Stack.
A pilha está num espaço virtual de memória O valor em ESP é um endereço virtual. Se, por exemplo, no início for 64FE3Ch, não estará se referindo a um endereço de memória existente na memória física real. Para obter o endereço físico da memória, o sistema precisa converter (ou "mapear") 64FE3Ch de acordo com seus próprios registros internos. Por exemplo, este endereço pode muito bem corresponder a 2FE3Ch na memória física real. Portanto, um endereço virtual é apenas uma representação conveniente de uma posição na memória. Costumase dizer que cada aplicação roda no seu próprio espaço virtual de endereços. Na teoria, toda a extensão de endereços de 32 bits (zero a 4 Gb) está disponível para cada uma das
aplicações. Na prática a coisa muda de figura, mas continua sendo verdade que cada aplicação que esteja rodando no sistema pode usar a mesma extensão de endereços virtuais. Não ocorrem conflitos por que o sistema sabe o tempo todo qual aplicação está endereçando memória. Portanto, pode indicar às aplicações o local correto na memória física. Deste modo, é possível que várias aplicações apresentem simultaneamente o mesmo valor em ESP porém cada um destes valores esteja apontando para um local diferente da memória física.
Conteúdo inicial da pilha Quando é carregado, o sistema operacional Windows aloca uma área de pilha específica para a linha ( thread) principal. O próprio sistema faz uso deste thread e da sua área de pilha antes de chamar o endereço de entrada do programa. Você pode ver isto no debugger. Inicie seu programa, deixe chegar no endereço de entrada e observe o valor de ESP. Agora abra uma janela de inspeção para o valor de ESP. Talvez você imagine estar na base da área de memória, só que não é este o caso. Se você rolar a janela de inspeção para a base da memória (role para o maior endereço) você verá que já houve muita atividade na pilha durante a preparação do sistema para chamar o endereço de entrada do programa. É interessante que o último valor da pilha, antes da aplicação ser chamada, é um endereço de retorno na Kernel32.dll. Isto indica que uma função da Kernel32.dll chamou a aplicação. Devido à existência deste endereço de retorno é possível usar um simples RET para terminar o processo, ao invés de chamar ExitProcess. É claro que isto só funciona se a pilha estiver em equilíbrio de modo que a execução do código continue na função chamadora da Kernel32.dll. Um pouco mais adiante podemos ver na pilha o nome do arquivo da aplicação e, mais adiante ainda, podemos observar o endereço do manipulador de exceções que o próprio sistema alocou para o thread principal da aplicação. Todas estas coisas mostram que a área de pilha da aplicação (assim como seu thread) é utilizada pelo sistema para preparar a chamada a esta aplicação.
Espaço inicial da pilha No Windows, quando alguma memória é reservada para o uso de uma aplicação, uma certa quantidade de endereços virtuais são alocados pelo sistema. Esta alocação preserva estes endereços para que a aplicação possa utilizá-los. Se a aplicação precisar de mais memória, os mesmos endereços não podem ser reutilizados. Nenhuma memória física é utilizada enquanto a memória não tiver sido consignada. Neste ponto, os endereços virtuais que foram alocados são mapeados para a área ou áreas da memória física que estejam disponíveis para o sistema. Obviamente, para que este processo funcione, o sistema precisa saber do tamanho máximo de memória contígua que pode ser consignada. Esta passa a ser a extensão de endereços alocados. O mesmo se aplica quando alguma memória é reservada para o uso da pilha. No início de uma aplicação, o sistema precisa saber quanta memória alocará para a pilha e quanto deverá consignar na primeira instância. Estas duas quantidades estão referenciadas no arquivo PE em +48h e +4Ch no cabeçalho opcional. Como veremos abaixo, referem-se
não somente ao thread principal da aplicação, mas também a novos threads criados pela aplicação. A maioria dos linkers utilizam, respectivamente, 1Mb e 4 Kb (o tamanho normal de página) para estes valores. Com o GoLink você pode alterar estes valores default usando, respectivamente, /stacksize e /stackinit (veja o manual do GoLink para saber como usá-los).
Ampliando a pilha em tempo de execução O sistema percebe quando uma aplicação está tentando ler ou escrever além da área consignada para a pilha usando manipulação de exceção. Considerando que a tentativa ocorra dentro da área permitida da pilha, mais memória será consignada de acordo com a necessidade. Mesmo que ocorra uma tentativa de aumentar a pilha além da área alocada, o sistema NT (mas não o Win9x) tentará alocar mais memória, o que só não ocorre se os endereços virtuais requeridos tenham sido alocados para outras áreas de memória.
Área de uso permitido 64D000h A pilha não é considerada apropriada para manter grandes quantidades de dados Página 4K 64F000h e este enfoque é reforçado disponível pelo Windows através do seu Página 4K 650000h mecanismo de exceção. No ESP (64FE3Ch) está aqui → Win9x, a área de pilha usável disponível permitida situa-se entre o ESP corrente e o limite da próxima página mais o tamanho da página. Por exemplo, se ESP for 64FE3Ch, então o limite da próxima página será 64F000h e o tamanho da página extra (que geralmente é fixada em 4K pelo sistema) nos leva para 64E000h.
Página 4K 64E000h indisponível
Desta forma, se ESP for 64FE3Ch, a instrução MOV D[ESP-1E40h],0
causará uma exceção por que o ponto atual da pilha que está sendo endereçado é 64DFFCh, uma área não disponível por que ainda não foi consignada pelo sistema. Também não é possível contornar o problema movendo ESP. No Win9x, o sistema permite que o ESP seja movido apenas até ao limite da próxima página + o tamanho da página menos quatro bytes. Por exemplo, se ESP for 64FE3Ch, só será permitida uma única instrução para mover ESP em 1E38h (em decimal isto corresponde a 7836 bytes). Isto significa que a instrução
SUB ESP,1E38h
faz com que ESP se torne 64E004h e isto é permitido. Já a instrução SUB ESP,1E3Ch
causará uma exceção. A diferença de 4 bytes na posição que dispara a exceção sugere que existem dois tipos de proteção. Pelo acima exposto pode parecer que o tamanho dos dados que podem ser colocados na pilha esteja limitado a 4K, porém isto não é verdade. Existem duas maneiras de se evitar estas exceções e, desta forma, usar a pilha para uma quantidade maior de dados. A primeira forma é mover e usar o ESP incrementalmente. Isto assegurará que o sistema consigne memória progressivamente, como desejado. O seguinte código cria com segurança uma área de 40K bytes na pilha: MOV ECX,10 L0: SUB ESP,1000h MOV D[ESP],0 LOOP L0
Aqui se obriga o sistema a consignar 10 blocos de 4K de memória de pilha. O ESP acaba ficando no topo desta área de pilha. Este processo não é particularmente rápido por que o sistema precisa consignar memória dez vezes. Um método mais rápido é instruir o sistema a consignar uma quantidade maior que a usual de memória para a pilha quando a aplicação for carregada. Com o GoLink é possível fazer isto usando /stackinit. Por exemplo, /stackinit 0A000
garantirá que 40K de memória sejam consignados para a pilha no início. Você vai poder mover o ESP com segurança usando a instrução SUB ESP,0A000h
e terá um espaço de 40K de memória para brincar.
Usando a pilha para manter um fluxo de dados Com as devidas precauções, a pilha pode ser usada para armazenar um fluxo razoável de dados. Os pontos que devem ser lembrados são:
Sempre restaure o equilíbrio de ESP quando tiver terminado a operação com a pilha. Nunca escreva para [ESP], a não ser que você tenha subtraído pelo menos 4 bytes do valor original de ESP, por que estes contém o endereço de retorno do procedimento. Nunca escreva para [ESP+n], a não ser que um número suficiente de bytes tenha
sido subtraído de ESP para evitar que outros dados importantes sejam sobrescritos. Se você não mover o ESP para o topo da área de dados então será preciso escrever os dados na direção inversa, isto é, para endereços progressivamente decrescentes. Isto pode ser feito de vários modos, o mais conveniente sendo provavelmente ativando a flag de direção usando STD e depois usando instruções MOV. Por exemplo: MOV MOV SUB STD REP CLD
ECX,8000 EDI,ESP EDI,4
MOVSD
; ativa a flag de direção ; move dwords de ECX de [ESI] para [EDI] ; limpa a flag de direção
Este código escreve 8.000 dwords na pilha. Observe como SUB EDI,4 evita a escrita sobre [ESP], o qual contém o endereço de retorno do procedimento. Não há problema com o aumento da memória por que a escrita é incremental e o sistema apropriadamente cria novas áreas de memória de 4K à medida que se tornam necessárias.
Se você mover ESP para o topo da área de dados, será preciso tomar as precauções citadas no tópico "Área de uso permitido". Respeitando as premissas será possível escrever na direção normal.
A pilha em aplicações multi-thread Cada thread do seu aplicativo possui seus próprios registradores e pilha. Isto quer dizer que, quando o sistema delegar tempo de processamento ao thread, ele entrará no contexto de registradores deste thread. O contexto contém todos os valores dos registradores existentes no momento em que, da última vez, o tempo de processamento foi tirado do thread. Como os registradores incluem o ESP, seu valor também será corretamente trocado de modo que a área de memória física correta será usada pelo thread como sua pilha. O resultado é que thread pode se apoiar no fato de que pode usar sua pilha como uma área particular da memória que recebe interferências de outros threads. Você pode observar isto no debugger. Será possível ver que o ESP sempre muda substancialmente quando a execução troca de thread. Quando um thread é iniciado, sua área de pilha é alocada. Como exemplo prático, verificou-se que o thread principal de um aplicativo rodava a partir de 64FE3Ch (para baixo) e, quando um novo thread era feito, sua pilha rodava a partir de 75FF9Ch (para baixo). Num outro teste, quando seis threads novos foram feitos, suas pilhas foram iniciadas respectivamente em 19DEF9Ch, 1AFFF9Ch, 1C1FF9Ch, 1D3FF9Ch, 1E5FF9Ch e 1F7FF9Ch. Aqui você pode notar que o sistema está separando o endereço virtual de cada área de pilha com 128Kb a mais do que o default de 1Mb. Provavelmente isto tenha ocorrido para abrir espaço para o uso da pilha pelo sistema e também alguma folga. Alterando a alocação do tamanho da pilha para 200000h (2Mb) através do uso de /stacksize e depois criando seis threads novos teve como resultado a
separação das áreas de pilha com 128Kb a mais que os 2 Mb.
Moldura da pilha e dados locais Uma moldura de pilha é uma área particular da pilha que contém um endereço de retorno de uma função e dados usados por esta função, sem o risco de sobre-escrita porque o valor de ESP foi decrementado. Os dados mantidos numa moldura da pilha são denominados "dados locais". Isto porque são usados apenas dentro da referida moldura e não está previsto que sejam endereçados pelo programa de forma geral. Vejamos este exemplo simples: PROCEDURE1: SUB ESP,20h
; faz espaço na pilha para dados locais ; usa a área de dados locais
CALL PROCEDURE2
ADD ESP,20h RET
; retorna de PROCEDURE2 ; continua a usar dados locais ; restaura o equilíbrio de ESP
e PROCEDURE2: PUSH EAX,EBX,ECX ;faz vários cálculos
POP ECX,EBX,EAX RET
Aqui a moldura da pilha é criada usando a instrução SUB ESP,20h. Isto diminui o valor de ESP em 32 bytes, criando espaço na pilha para 8 dwords. Agora, como o ESP foi mudado, qualquer coisa que ocorrer na PROCEDURE2 nunca vai sobre-escrever estes 8 dwords. Vamos conferir isto visualmente imaginando que ESP contenha 64FE38h no início da PROCEDURE1:
ESP aqui no início da PROCEDURE2→
Moldura de pilha da PROCEDURE1
64FE08h
contém valor em ECX inserido pela PROCEDURE2
64FE0Ch
contém valor em EBX inserido pela PROCEDURE2
64FE10h
contém valor em EAX inserido pela PROCEDURE2
64FE14h
contém o endereço de retorno da PROCEDURE2
64FE18h a 64FE34h
8 dwords para dados locais
64FE38h
contém o endereço de retorno da PROCEDURE1
Endereçando dados locais Observação: isto é automatizado no GoAsm usando FRAME..ENDF e no MASM usando PROC..ENDP.
Uma vez que ESP aponta para o topo da área de dados locais, é possível endereçar os dados usando ESP. Assim, no exemplo acima, o primeiro dado local dword estaria disponível em [ESP] imediatamente após o SUB ESP,20h. Porém, usando ESP para gerenciar os dados locais na pilha pode ser complicado por que ESP mudará a cada CALL ou PUSH dentro do procedimento. Por esta razão costuma-se usar o registrador EBP e não o ESP. Atribui-se um valor ao EBP logo no início da moldura de pilha, na base dos dados locais, e o valor não é alterado enquanto a execução não abandonar a moldura. Desta forma temos certeza de que os dados locais podem sempre ser endereçados usando um deslocamento (offset) de EBP. Agora, o código para uma moldura de pilha típica passa a ter a seguinte aparência: MolduraPilhaTipica: PUSH EBP ; salva o valor de ebp que será alterado MOV EBP,ESP ; põe valor atual do ponteiro de pilha em ebp SUB ESP,0Ch ; cria espaço para os dados locais ; ; PONTO "X" ; ; ; código dentro do procedimento ; MOV ESP,EBP ; restaura o ponteiro da pilha ; restaura o valor de ebp POP EBP RET ; retorna ao chamador ajustando o ponteiro da pilha
} } "prólogo" }
} } "epílogo" } }
Aqui mudamos o ponteiro da pilha em 12 bytes. No ponto "X", a pilha em relação a EBP tem o seguinte aspecto:
ESP aqui no ponto "X"→
ebp-10h
o próximo push entrará aqui
ebp0Ch
contém espaço para dado local
ebp-8h
contém espaço para dado local
ebp-4h
contém espaço para dado local
ebp
contém valor original de ebp
ebp+4h
contém o endereço de retorno de MolduraPilhaTipica
Agora, por todo a moldura de pilha, seja o que for que acontecer a ESP, os dados locais estarão acessíveis em [EBP-4H], [EBP-8h] e [EBP-0Ch]. Observe como o equilíbrio de ESP é restaurado automaticamente pelo uso de MOV ESP,EBP pouco antes de retornar ao chamador.
Não é obrigatório usar EBP para este fim, qualquer registrador é adequado. Acontece que o EBP é tradicionalmente usado para este fim e seu código será mais facilmente entendido por outros programadores.
Acessando parâmetros através da pilha Já vimos como passar parâmetros para outros procedimentos usando a pilha. Agora vamos analisar como usar parâmetros passados para procedimentos no seu próprio código. Basicamente, estes parâmetros estão mais em baixo na pilha para que não sejam sobre-escritos em circunstâncias normais. Por esta razão não há necessidade nenhuma de salvá-los ou resgatá-los. Depois de entrar num procedimento, ESP estará apontando para o endereço de retorno deste procedimento (inserido pelo CALL). Por isto, os parâmetros estarão em [ESP+4h], [ESP+8h], [ESP+0Ch] e assim por diante, dependendo de quantos parâmetros existirem. Mas pode ser difícil localizar onde exatamente estejam os parâmetros usando ESP por que o valor deste registrador mudará no próximo PUSH ou CALL. Mais uma vez o EBP pode ser usado para apontar os parâmetros. Se o código prólogo for PUSH EBP MOV EBP,ESP SUB ESP,0Ch
; salva o valor de ebp que será alterado } ; põe valor do ponteiro da pilha atual em ebp } "prólogo" ; cria espaço para dados locais }
quando ESP for passado para EBP, ele terá 4 bytes a menos do que tinha no início da chamada (devido ao primeiro PUSH EBP). Portanto, os parâmetros agora podem ser acessados usando [EBP+8h], [EBP+0Ch], [EBP+10h] e assim por diante, dependendo do número de parâmeros existentes.
A pilha e procedimentos callback do Windows As duas técnicas utilizadas (criando espaço para dados locais e endereçando parâmetros) são requeridas em procedimentos de callback do Windows. O procedimento callback mais comum em programas Windows é o procedimento janela. É para este procedimento que o Windows envia "mensagens" e o Windows espera uma resposta correta. O que acontece neste caso é que o Windows chama o procedimento janela usando o thread do próprio programa. Isto geralmente ocorre enquanto o programa estiver num loop de mensagem esperando um retorno da API GetMessage ou então executando a API DispatchMessage. Por sorte você pode usar FRAME..ENDF no GoAsm para obter os parâmetros enviados por janelas e também endereçá-los por nome. Você também pode criar com facilidade áreas de dados locais endereçáveis por nome. E você pode preservar registradores e restaurar o equilíbrio da pilha automaticamente. Veja o manual do GoAsm para uma descrição completa.
Fonte
Não posso deixar de agradecer Jeremy Gordon pelos seus excelentes artigos sobre a pilha. O presente texto é (praticamente) apenas a tradução de Understand the stack (part 2).
Índice do Artigo Janelas (masm) Janelas II Janelas III Janelas IV Pilotando a janela Pilotando a janela II Código fonte Conferindo o trabalho do MASM Todas as páginas Página 1 de 8
No tutorial anterior aprendemos como pôr uma janelinha na tela. Só que era uma janela do tipo message box, que não permite adicionar certas funcionalidades como um menu por exemplo. Neste tutorial vamos aprender a criar uma janela "de verdade". Além disso, vamos explorar a "mecânica" da coisa.
Projeto Como sempre, nosso primeiro passo é planejar o programa. O projeto até que é pequeno, mas, em compensação... o tutorial é um mastodonte! 1. O sistema operacional é o Windows de 32 bits. 2. Não precisamos mais do que o conjunto de instruções do processador 386. 3. Vamos criar apenas uma janela "nua", com um mínimo de funcionalidade (apenas pode ser fechada). Vamos por partes que a coisa há de ficar clara. Um dos aspectos mais importantes é entender o sistema de comunicação do Windows. Antes de começar pra valer este tutorial que deve ficar um pouco longo, abra o QEditor do MASM32 e digite (ou copie
e cole) o esqueleto de um programa que possa ser finalizado com ExitProcess: .386 .MODEL FLAT,STDCALL option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .CODE inicio: invoke ExitProcess,0 end inicio
A teoria das janelas A Interface Gráfica dos programas Windows, conhecida como GUI (Graphical User Interface), depende essencialmente de funções da API. O uso desta interface padrão beneficia usuários e programadores. Para os usuários, as GUIs dos programas Windows são todas parecidas facilitando a navegação. Para os programadores, os códigos da GUI estão testados e prontos para uso. A desvantagem para os programadores é a complexidade crescente envolvida. Para criar ou manipular qualquer objeto GUI, como janelas, menus ou ícones, os programadores precisam seguir regras rígidas definidas pelo sistema Windows. Logo abaixo estão os passos necessários para se criar uma janela no desktop: 1. Obter um manipulador de instância - HANDLE (obrigatório). 2. Pegar as instruções da linha de comando (dispensável, a não ser que o programa necessite de parâmetros iniciais fornecidos pelo usuário). 3. Criar e Registrar a classe da janela (requerido, a não ser que se use tipos de janelas predefinidos, por exemplo MessageBox ou uma Dialog Box). 4. Criar a janela (Obrigatório). 5. Mostrar a janela no desktop. 6. Atualizar a área cliente da janela. 7. Entrar num loop infinito checando as mensagens do Windows. 8. Processar as mensagens da janela - o gerente de mensagens. 9. Encerrar o programa se o usuário fechar a janela.
Handle Como usuário do Windows você sabe que pode rodar várias instâncias de um mesmo programa. Se você abrir a calculadora do Windows duas vezes, obterá duas janelas distintas, cada uma delas rodando uma instância do programa. Se você fizer cálculos na primeira janela, a segunda não é afetada. Portanto, cada uma das instâncias, aos olhos do sistema, é um aplicativo independente. Como é que o Windows consegue individualizar cada uma das instâncias? Através de um identificador
ou manipulador de instância. Este manipulador é apenas um número que identifica a instância para o sistema. Por exemplo, "a janela da instância 1 precisa ser atualizada", "a janela da instância 2 foi ativada", "a janela da instância 2 foi minimizada", etc, permite que o sistema efetue as tarefas nas janelas corretas. Se não existisse o número de identificação, bem... a bagunça seria inevitável. E se duas instâncias possuírem o mesmo número... o Windows vai dar pau. Existe uma função da API que nos permite obter um manipulador de instância que não conflite com outros já existentes (as duas janelas que abrimos para a calculadora não são as únicas que estão abertas). Esta função é a GetModuleHandle, que faz parte da kernel32.lib. Aproveite e familiarize-se um pouco mais com o MASM32: clique no item de menu [Tools / API Library List] para ativar uma ferramenta muito útil que nosso amigo hutch colocou à nossa disposição - a "API to Library list". Esta janelinha é o mapa da mina que relaciona uma grande quantidade de funções e as bibliotecas correspondentes. Digite "getmoduleh" e você já estará na linha que corresponde à função procurada: GetModuleHandle lib ==> kernel32.lib Nós (e o sistema) vamos precisar do manipulador de instância quando quisermos atualizar ou alterar alguma coisa na(s) janela(s) do programa. É um número que será usado numa porção de funções diferentes, portanto, precisamos preparar um lugar onde vamos guardar este identificador e que possa ser acessado de qualquer ponto do programa . Um endereço de memória que guarda um valor e que pode ser acessado de qualquer ponto do programa é chamado de VARIÁVEL GLOBAL. Ao invés de lembrar do endereço, podemos dar um nome a ele. Por exemplo, você pode ir para a casa do "Zé" ou para a "rua dos bits, número 135" que dá na mesma, pois o Zé mora neste endereço. Já que precisamos deixar a critério do sistema o número que será designado como manipulador de instância para o programa, o valor da variável que conterá o manipulador da instância só pode ser obtido em tempo de execução, portanto, não podemos (e não devemos!) inicializar esta variável. A seção para variáveis não inicializadas, conforme já foi visto no tutorial "Por onde começar", é a seção .DATA?. Nesta seção damos nomes às variáveis, indicamos seu tipo e informamos que não são inicializadas através de um ponto de interrogação (?).
A função GetModuleHandle Qual é o tipo da variável que precisamos declarar? A referência da API nos diz que a função GetModuleHandle retorna um manipulador de módulo (módulo é igual a instância no win32) para o módulo especificado se o arquivo tiver sido mapeado no espaço de endereços do processo chamador e que o tipo do valor de retorno é HMODULE - apenas um dos muitos nomes que o Windows dá a DWORD. HMODULE GetModuleHandle( LPCTSTR lpModuleName // endereço do nome do módulo para o qual se solicita um manipulador );
Parâmetro lpModuleName: Aponta para uma string terminada em zero com o nome de um módulo Win32 (uma . DLL ou arquivo .EXE). Se a extensão do nome do arquivo for omitida, a extensão default é .DLL. A string do nome do arquivo por ter um caracter finalizador ponto (.) para indicar que o nome do módulo não possui extensão. A string não precisa especificar um caminho (path). O nome é comparado (sem considerar maiúsculas e minúsculas) aos nomes dos módulos que já estejam mapeados no espaço de endereços do processo chamador. Se o parâmetro for NULL, GetModuleHandle retorna um manipulador do arquivo usado para criar o processo da chamada.
Então vamos digitar um pouco. Certifique-se de que os arquivos kernel32.inc e kernel32.lib constam na lista de include e includelib. Uma vez familiarizados com a função, podemos chamá-la com o parâmetro NULL. É tudo o que queremos: um manipulador de instância para o nosso programa. .386 .MODEL FLAT,STDCALL option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .DATA? mInstancia DWORD ? .CODE inicio: invoke GetModuleHandle, NULL mov mInstancia, eax invoke ExitProcess,0 end inicio
A diretiva invoke você já conhece do tutorial "O Folgado" e a função GetModuleHandle já foi mais do que explicada. A linha seguinte é que são elas: usa o mnemônico MOV com os operandos mInstancia e eax. Vamos por partes. Um mnemônico é um nome reservado de uma família de códigos operacionais que realizam tarefas semelhantes no processador. MOV pede ao processador para MOVer ou copiar um valor de um local para outro. Dê uma olhada no texto acessório "Códigos Operacionais" para entender melhor. EAX é um registrador de uso geral, ou seja, é um tipo especial de memória DENTRO do processador e que serve para o armazenamento temporário de dados (a informação pode ser colocada num determinado instante e de lá ser retirada quando isso se fizer necessário). Existem vários registradores dentro do processador - para maiores detalhes leia o texto acessório "Registradores". Em todo caso, esta nova linha pode ser traduzida da seguinte maneira: MOVa o valor que se encontra no registrador EAX para a posição de memória referente à nossa
variável mInstancia. Quando esta linha for executada, a variável global mInstancia é inicializada. O motivo pelo qual transferimos o valor de EAX para mInstancia é que o valor de retorno das funções da API são armazenados no registrador EAX. Neste caso, assim que se volta da função GetModuleHandle, o registrador EAX contém o valor do manipulador de instância solicitado.
Índice do Artigo Janelas (masm) Janelas II Janelas III Janelas IV Pilotando a janela Pilotando a janela II Código fonte Conferindo o trabalho do MASM Todas as páginas No tutorial anterior aprendemos como pôr uma janelinha na tela. Só que era uma janela do tipo message box, que não permite adicionar certas funcionalidades como um menu por exemplo. Neste tutorial vamos aprender a criar uma janela "de verdade". Além disso, vamos explorar a "mecânica" da coisa.
Projeto Como sempre, nosso primeiro passo é planejar o programa. O projeto até que é pequeno, mas, em compensação... o tutorial é um mastodonte! 1. O sistema operacional é o Windows de 32 bits. 2. Não precisamos mais do que o conjunto de instruções do processador 386. 3. Vamos criar apenas uma janela "nua", com um mínimo de funcionalidade (apenas pode ser fechada). Vamos por partes que a coisa há de ficar clara. Um dos aspectos mais importantes é entender o sistema de comunicação do Windows. Antes de começar pra valer este tutorial que deve ficar um pouco longo, abra o QEditor do MASM32 e digite (ou copie
e cole) o esqueleto de um programa que possa ser finalizado com ExitProcess: .386 .MODEL FLAT,STDCALL option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .CODE inicio: invoke ExitProcess,0 end inicio
A teoria das janelas A Interface Gráfica dos programas Windows, conhecida como GUI (Graphical User Interface), depende essencialmente de funções da API API.. O uso desta interface padrão beneficia usuários e programadores. Para os usuários, as GUIs dos programas Windows são todas parecidas facilitando a navegação. Para os programadores, os códigos da GUI estão testados e prontos para uso. A desvantagem para os programadores é a complexidade crescente envolvida. Para criar ou manipular qualquer objeto GUI, como janelas, menus ou ícones, os programadores programadores precisam seguir regras rígidas definidas pelo sistema Windows. Logo abaixo estão os passos necessários para se criar uma janela no desktop: 1. Obter um manipulador de instância - HANDLE (obrigatório). 2. Pegar as instruções da linha de comando (dispensável, a não ser que o programa necessite de parâmetros iniciais fornecidos pelo usuário). 3. Criar e Registrar a classe da janela (requerido, a não ser que se use tipos de janelas predefinidos, predefinidos, por exemplo MessageBox MessageBox ou uma Dialog Box). 4. Criar a janela (Obrigatório). 5. Mostrar a janela no desktop. 6. Atualizar a área cliente da janela. 7. Entrar num loop infinito checando as mensagens do Windows. 8. Processar as mensagens da janela - o gerente de mensagens. 9. Encerrar o programa se o usuário fechar a janela.
Handle Como usuário do Windows você sabe que pode rodar várias instâncias de um mesmo programa. Se você abrir a calculadora do Windows duas vezes, obterá duas janelas distintas, cada uma delas rodando uma instância do programa. Se você fizer cálculos na primeira janela, a segunda não é afetada. Portanto, cada uma das instâncias, aos olhos do sistema, é um aplicativo independente. Como é que o Windows consegue individualizar cada uma das instâncias? Através de um identificador
ou manipulador de instância. Este manipulador é apenas um número que identifica a instância para o sistema. Por exemplo, "a janela da instância 1 precisa ser atualizada", "a janela da instância 2 foi ativada", "a janela da instância 2 foi minimizada", etc, permite que o sistema efetue as tarefas nas janelas corretas. Se não existisse o número de identificação, bem... a bagunça seria inevitável. E se duas instâncias possuírem o mesmo número... o Windows vai dar pau. Existe uma função da API que nos permite obter um manipulador de instância que não conflite com outros já existentes (as duas janelas que abrimos para a calculadora não são as únicas que estão abertas). Esta função é a GetModuleHandle, que faz parte da kernel32.lib. Aproveite e familiarize-se um pouco mais com o MASM32: clique no item de menu [Tools / API Library List] para ativar uma ferramenta muito útil que nosso amigo hutch colocou à nossa disposição - a "API to Library list". Esta janelinha é o mapa da mina que relaciona uma grande quantidade de funções e as bibliotecas correspondentes. Digite "getmoduleh" e você já estará na linha que corresponde à função procurada: GetModuleHandle lib ==> kernel32.lib Nós (e o sistema) vamos precisar do manipulador de instância quando quisermos atualizar ou alterar alguma coisa na(s) janela(s) do programa. É um número que será usado numa porção de funções diferentes, portanto, precisamos preparar um lugar onde vamos guardar este identificador e que possa ser acessado de qualquer ponto do programa . Um endereço de memória que guarda um valor e que pode ser acessado de qualquer ponto do programa é chamado de VARIÁVEL GLOBAL. Ao invés de lembrar do endereço, podemos dar um nome a ele. Por exemplo, você pode ir para a casa do "Zé" ou para a "rua dos bits, número 135" que dá na mesma, pois o Zé mora neste endereço. Já que precisamos deixar a critério do sistema o número que será designado como manipulador de instância para o programa, o valor da variável que conterá o manipulador da instância só pode ser obtido em tempo de execução, portanto, não podemos (e não devemos!) inicializar esta variável. A seção para variáveis não inicializadas, conforme já foi visto no tutorial "Por onde começar", é a seção .DATA?. Nesta seção damos nomes às variáveis, indicamos seu tipo e informamos que não são inicializadas inicializadas através de um ponto de interrogação (?).
A função GetModuleHandle Qual é o tipo da variável que precisamos declarar? A referência da API nos diz que a função GetModuleHandle GetModuleHandle retorna um manipulador de módulo (módulo é igual a instância no win32) para o módulo especificado se o arquivo tiver sido mapeado no espaço de endereços do processo chamador e que o tipo do valor de retorno é HMODULE - apenas um dos muitos nomes que o Windows dá a DWORD. HMODULE GetModuleHandle( LPCTSTR lpModuleName // endereço do nome do módulo para o qual se solicita um manipulador );
Parâmetro lpModuleName: Aponta para uma string terminada em zero com o nome de um módulo Win32 (uma . DLL ou arquivo .EXE). Se a extensão do nome do arquivo for omitida, a extensão default é .DLL. A string do nome do arquivo por ter um caracter finalizador ponto (.) para indicar que o nome do módulo não possui extensão. A string não precisa especificar um caminho (path). O nome é comparado (sem considerar maiúsculas e minúsculas) aos nomes dos módulos que já estejam mapeados no espaço de endereços do processo chamador. Se o parâmetro for NULL, GetModuleHandle retorna um manipulador do arquivo usado para criar o processo da chamada.
Então vamos digitar um pouco. Certifique-se de que os arquivos kernel32.inc e kernel32.lib constam na lista de include e includelib. Uma vez familiarizados com a função, podemos chamá-la com o parâmetro NULL. É tudo o que queremos: um manipulador de instância para o nosso programa. .386 .MODEL FLAT,STDCALL option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .DATA? mInstancia DWORD ? .CODE inicio: invoke GetModuleHandle, NULL mov mInstancia, eax invoke ExitProcess,0 end inicio
A diretiva invoke você já conhece do tutorial "O Folgado" e a função GetModuleHandle já foi mais do que explicada. A linha seguinte é que são elas: usa o mnemônico MOV com os operandos mInstancia e eax. Vamos por partes. Um mnemônico é um nome reservado de uma família de códigos operacionais que realizam tarefas semelhantes no processador. MOV pede ao processador para MOVer ou copiar um valor de um local para outro. Dê uma olhada no texto acessório "Códigos Operacionais" Operacionais" para entender melhor. EAX é um registrador de uso geral, ou seja, é um tipo especial de memória DENTRO do processador e que serve para o armazenamento temporário de dados (a informação pode ser colocada num determinado instante e de lá ser retirada quando isso se fizer necessário). necessário). Existem vários registradores registradores dentro do processador processador - para maiores detalhes leia o texto acessório "Registradores". Em todo caso, esta nova linha pode ser traduzida da seguinte maneira: MOVa o valor que se encontra no registrador EAX para a posição de memória referente à nossa
variável mInstancia. Quando esta linha for executada, a variável global mInstancia é inicializada. O motivo pelo qual transferimos o valor de EAX para mInstancia é que o valor de retorno das funções da API são armazenados no registrador EAX. Neste caso, assim que se volta da função GetModuleHandle, o registrador EAX contém o valor do manipulador de instância solicitado.
Instruções da linha de comando A linha de comando geralmente se resume no nome do programa que queremos executar, ou seja, não contém parâmetros adicionais. Existem alguns raros casos em que é necessário enviar um ou alguns parâmetros para que o programa funcione corretamente ou de forma personalizada. Somente nestes raros casos é que precisamos usar a função da API GetCommandLine, também da kernel32.lib. Apenas a título de ilustração vamos inserir esta chamada. .386 .MODEL FLAT,STDCALL option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .DATA? mInstancia DWORD ? linhaComando DWORD ? .CODE inicio: invoke GetModuleHandle, NULL mov mInstancia, eax invoke GetCommandLine mov linhaComando, eax invoke ExitProcess,0 end inicio
A classe da janela O Windows só consegue criar objetos a partir de um modelo - é como se o sistema precisasse da "planta da casa" para que poder construir a "casa". Primeiro é preciso criar a "planta" e depois entregá-la (fazer o registro) para que o Windows possa usá-la como modelo. Normalmente a função padrão utilizada para esta dupla tarefa (criar e registrar) é a WinMain, que é chamada pelo sistema como ponto de entrada para um aplicativo
win32. A referência da API nos mostra o seguinte: seguinte: int WINAPI WinMain( HINSTANCE hInstance, // manipulador da instância HINSTANCE hPrevInstance, // manipulador da instância anterior LPSTR lpCmdLine, // ponteiro para a linha de comando int nCmdShow // modo de apresentação da janela );
Para poder utilizar esta função precisamos criar o protótipo da mesma. Se você esqueceu o que é um protótipo de função, refresque a memória relendo "O Folgado". Para criar o protótipo é necessário conhecer os tipos dos parâmetros que a função espera receber: HINSTANCE, LPSTR e int. Na verdade, todos eles são nomes diferentes que o Windows dá ao DWORD. Esta função recebe quatro parâmetros: o manipulador da instância do nosso programa, o manipulador de instância da instância anterior do nosso programa, a linha de comando e o estado da janela da primeira vez em que aparecer. No win32, NÃO existe uma instância anterior. Cada programa está sozinho no seu espaço de endereços, de modo que o valor de hPrevInstance será sempre 0 (NULL). Isto é uma sobra da época do win16 quando todas as instâncias de diversos programas rodavam no mesmo espaço de endereços e uma instância queria saber se era a primeira. No win16, se hPrevInstance for NULL, então esta instância é a primeira. O nome da função também pode ser um da nossa escolha, portanto, nosso protótipo pode ser gerenteJanela proto :DWORD, :DWORD, :DWORD, :DWORD
O manipulador da instância do nosso programa já está armazenado em mInstancia, o manipulador da instância anterior é NULL, o ponteiro para a linha de comando já está em linhaComando e o modo de exibição pode ser o padrão (SW_SHOWDEFAULT). Nosso código fonte passa a ser o seguinte: .386 .MODEL FLAT,STDCALL option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib gerenteJanela proto :DWORD, :DWORD, :DWORD, :DWORD .DATA? mInstancia DWORD ? linhaComando DWORD ? .CODE inicio: invoke GetModuleHandle, NULL mov mInstancia, eax invoke GetCommandLine mov linhaComando, eax invoke gerenteJanela, mInstancia, NULL, linhaComando, SW_SHOWDEFAULT invoke ExitProcess,0
end inicio gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD ... gerenteJanela endp
A função gerenteJanela é um procedimento que separamos do corpo principal do código (que fica entre o par de rótulos "inicio:" e "end inicio"). Fiz isto apenas para destacar e individualizar este procedimento do resto do código. Se você quiser, não precisa criar o protótipo da função, não precisa fazer o invoke e nem criar o procedimento gerenteJanela. Pode simplesmente colocar todo o código que vem a seguir no corpo principal. Além disso, se você suprimiu o código correspondente à linha de comando, basta enviar um parâmetro NULL no lugar de linhaComando.
A estrutura WNDCLASSEX A "planta" da classe da nossa janela é "desenhada" numa estrutura. Uma estrutura agrupa dados de tal forma que possam ser endereçados num bloco único (Leia mais sobre estruturas em "Trabalhando com Estruturas"). Usaremos uma estrutura predefinida no Windows, chamada WNDCLASSEX. A estrutura WNDCLASSEX foi planejada para conter todas as informações de uma classe janela e a referência da API nos mostra o seguinte: typedef struct _WNDCLASSEX { UINT cbSize; UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HANDLE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCTSTR lpszMenuName; LPCTSTR lpszClassName; HICON hIconSm; } WNDCLASSEX;
A seguir, a explicação para cada um dos membros desta estrutura:
cbSize: O tamanho da estrutura WNDCLASSEX em bytes. Podemos usar o operador SIZEOF para obter este valor. style: O estilo da janela criada a partir desta classe. Você pode combinar diversos estilos usando o operador "or". lpfnWndProc: O endereço do procedimento da janela (window procedure) responsável pelas janelas criadas a partir desta classe. cbClsExtra: Especifica o número de bytes extras que podem ser alocados logo
após a estrutura. O sistema operacional inicializa os bytes com zero. Aqui você pode armazenar dados específicos da classe. cbWndExtra: Especifica o número de bytes extras que podem ser alocados logo após a instância da janela. O sistema operacional inicializa estes bytes com zero. Se o aplicativo usar a estrutura WNDCLASS para registrar uma caixa de diálogo usando a diretiva CLASS do arquivo de recursos, ele precisa indicar este membro como DLGWINDOWEXTRA. hInstance: O manipulador da instância do módulo (programa). hIcon: O manipulador do ícone. Obtenha-o através da chamada de LoadIcon. hCursor: O manipulador do cursor. Obtenha-o através da chamada de LoadCursor. hbrBackground: A cor de fundo das janelas criadas a partir desta classe. lpszMenuName: O manipulador de menu default para janelas criadas a partir desta classe. lpszClassName: O nome desta classe. hIconSm: O manipulador do ícone pequeno associado a esta classe. Se este membro for NULL, o sistema procura nos recursos de ícones especificado pelo membro hIcon por um ícone de tamanho apropriado que possa ser usado.
Declaração Como só vamos precisar desta estrutura no procedimento da função gerenteJanela, vamos declará-la como variável LOCAL com o nome de ej (de estrutura janela - ou qualquer outro da sua escolha). A diretiva LOCAL aloca memória da pilha para esta variável e precisa estar situada imediatamente após a diretiva PROC. A sintaxe é LOCAL :. Vamos usar LOCAL ej:WNDCLASSEX, que pede ao MASM para alocar uma quantidade de memória de pilha correspondente ao tamanho da estrutura WNDCLASSEX para a variável de nome ej. A vantagem é que podemos referenciar ej no nosso código sem nos preocuparmos com o realinhamento da pilha, o que é mordomia pura. Uma desvantagem é que variáveis locais não podem ser usadas fora das funções onde foram criadas e que serão imediatamente destruídas quando se retorna ao chamador. Outra desvantagem é que variáveis locais não podem ser inicializadas automaticamente porque elas são apenas memória de pilha alocada dinamicamente na entrada da função. Precisamos atribuir seus valores manualmente após a diretiva LOCAL. Entre os membros da estrutura encontramos o que deve conter o ponteiro para o nome da classe que desejamos criar. Para preencher este requisito, precisamos inicializar uma variável na seção .DATA que contenha o nome da classe. .386 .MODEL FLAT,STDCALL option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib
gerenteJanela proto :DWORD, :DWORD, :DWORD, :DWORD .DATA NomeClasse db "JanelaNua",0 .DATA? mInstancia DWORD ? linhaComando DWORD ? .CODE inicio: invoke GetModuleHandle, NULL mov mInstancia, eax invoke GetCommandLine mov linhaComando, eax invoke gerenteJanela, mInstancia, NULL, linhaComando, SW_SHOWDEFAULT invoke ExitProcess,0 end inicio gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX gerenteJanela endp
A função A partir deste ponto listarei apenas a porção da função gerenteJanela. O primeiro membro da estrutura é o cbSize, que deve conter o tamanho da estrutura. Podemos obter este valor usando o operador SIZEOF. Como estilo da janela usaremos CS_HREDRAW OR CS_VREDRAW. O terceiro membro, lpfnWndProc, é o mais importante de todos. O significado de lpfn é "long pointer to function", ou seja, ponteiro longo para função. No win32 não existem ponteiros "near" (perto) ou "far" (distante); devido ao modelo de memória FLAT, existem apenas ponteiros. Mas isto, novamente, é sucata da época do win16. Cada classe janela precisa estar associada a uma função que gerencie o comportamento das janelas criadas a partir desta classe. Esta função, que chamaremos de gerenteMensagem, é tão importante que será discutida em separado. Por enquanto vamos atribuir valores aos primeiros membros da estrutura usando o mnemônico MOV: gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX mov ej.cbSize, SIZEOF WNDCLASSEX mov ej.style, CS_HREDRAW or CS_VREDRAW mov ej.lpfnWndProc, OFFSET gerenteMensagem mov ej.cbClsExtra, NULL mov ej.cbWndExtra, NULL gerenteJanela endp
O ej.hInstance, o próximo membro da estrutura, deve conter o manipulador da instância do programa. Podemos usar a variável global mInstancia ou o parâmetro mInst recebido pela função gerenteJanela pois ambos apontam para o mesmo endereço, ou seja, contém
o mesmo valor. Como não é possível transferir diretamente o valor de uma posição de memória para outra posição de memória e tanto ej.hInstance quanto mInst são posições de memória, será preciso usar o auxílio de um registrador. O mais fácil é utilizar o registrador da pilha: usar o mnemônico pushpara colocar o valor na pilha e o mnemônico pop para transferí-lo da pilha para ej.hInstance. gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX mov ej.cbSize, SIZEOF WNDCLASSEX mov ej.style, CS_HREDRAW or CS_VREDRAW mov ej.lpfnWndProc, OFFSET gerenteMensagem mov ej.cbClsExtra, NULL mov ej.cbWndExtra, NULL push mInst pop ej.hInstance gerenteJanela endp
Ícone e Cursor Para obter o manipulador do ícone e do cursor basta fazer uma chamada para LoadIcon e LoadCursor. A função LoadIcon carrega o recurso do ícone especificado a partir do executável associado a uma instância do aplicativo. HICON LoadIcon( HINSTANCE hInstance, LPCTSTR lpIconName // string com o nome do ícone ou identificador do recurso do ícone );
A função LoadCursor funciona como a anterior, apenas direcionada para o cursor. HCURSOR LoadCursor( HINSTANCE hInstance, LPCTSTR lpCursorName // string com o nome do cursor ou identificador do recurso do cursor );
Nas duas funções, HINSTANCE identifica uma instância do módulo cujo arquivo executável contém o ícone ou cursor que deve ser carregado. Como ainda não programamos os recursos do nosso aplicativo (vamos ver isto em tutoriais posteriores), nosso executável está "vazio" de recursos e HINSTANCE pode ser NULL. Neste caso serão usados os recursos do Windows e podemos usar os parâmetros default (veja mais detalhes na referência da API). Lembre-se de que o valor de retorno destas funções vai sempre para o registrador EAX.
Inicialização A ordem de inicialização dos membros da estrutura WNDCLASSEX não é importante.
Como estamos trabalhando o ícone do programa, aproveitaremos a chamada a LoadIcon e inicializaremos hIcon e hIconSm numa tacada. gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX mov ej.cbSize, SIZEOF WNDCLASSEX mov ej.style, CS_HREDRAW or CS_VREDRAW mov ej.lpfnWndProc, OFFSET gerenteMensagem mov ej.cbClsExtra, NULL mov ej.cbWndExtra, NULL push mInst pop ej.hInstance invoke LoadIcon, NULL, IDI_WINLOGO mov ej.hIcon, eax mov ej.hIconSm, eax invoke LoadCursor, NULL, IDC_ARROW mov ej.hCursor, eax gerenteJanela endp
A referência da API para WNDCLASSEX nos diz que, se usarmos uma cor para o membro hbrBackground, o valor da cor precisa ser um dos valores das cores padrão do sistema, acrescido de 1. Escolhemos a cor padrão COLOR_WINDOW, portanto usaremos COLOR_WINDOW+1. Como não projetamos os recursos, também não temos um menu para o nosso aplicativo - a string com o nome do menu, por enquanto, será NULL. E, finalmente, o nome da nossa classe de janela já foi definida na seção .DATA e o ponteiro para a string que contém o nome é OFFSET NomeClasse. gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX mov ej.cbSize, SIZEOF WNDCLASSEX mov ej.style, CS_HREDRAW or CS_VREDRAW mov ej.lpfnWndProc, OFFSET gerenteMensagem mov ej.cbClsExtra, NULL mov ej.cbWndExtra, NULL push mInst pop ej.hInstance invoke LoadIcon, NULL, IDI_WINLOGO mov ej.hIcon, eax mov ej.hIconSm, eax invoke LoadCursor, NULL, IDC_ARROW mov ej.hCursor, eax mov ej.hbrBackground, COLOR_WINDOW+1 mov ej.lpszMenuName, NULL mov ej.lpszClassName, OFFSET NomeClasse gerenteJanela endp
Foi extenso, porém não foi complicado. Nossa estrutura WNDCLASSEX está com todos os valores inicializados, ou seja, nossa classe de janela está definida. Mantendo a comparação inicial, a "planta da casa" está pronta. Agora podemos registrá-la.
Esta função é responsável pelo gerenciamento das mensagens provenientes de todas as janelas criadas a partir da classe associada. O Windows enviará mensagens à função para notificá-la de eventos importantes (entradas de teclado, cliques do mouse, etc) relativos às janelas pelas quais é responsável e a função deve responder adequadamente cada mensagem recebida.
Registro da classe Após criar uma classe (a "planta de uma casa"), é obrigatório registrá-la para que o sistema permita usá-la como modelo para criar uma ou mais instâncias de objetos ("uma ou mais casas") baseados nesta classe. Como utilizamos a estrutura WNDCLASSEX para definir as caracteríticas da nossa classe de janela, para registrá-la precisamos usar a função que "faz par" com ela: a RegisterClassEx. Caso tivéssemos utilizado WNDCLASS, a função para registro seria RegisterClass. ATOM RegisterClassEx( CONST WNDCLASSEX *lpwcx // ponteiro para a estrutura com os dados da classe );
Tranquilo. Temos a estrutura pronta e a função pede um ponteiro. Usando invoke podemos usar o operador ADDR para fornecê-lo. Acontece que esta função faz parte da user32.lib e esta biblioteca, assim como o arquivo include correspondente, precisa ser incluída: ... include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib ... gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX mov ej.cbSize, SIZEOF WNDCLASSEX mov ej.style, CS_HREDRAW or CS_VREDRAW mov ej.lpfnWndProc, OFFSET gerenteMensagem mov ej.cbClsExtra, NULL mov ej.cbWndExtra, NULL push mInst pop ej.hInstance invoke LoadIcon, NULL, IDI_WINLOGO mov ej.hIcon, eax mov ej.hIconSm, eax invoke LoadCursor, NULL, IDC_ARROW mov ej.hCursor, eax mov ej.hbrBackground, COLOR_WINDOW+1 mov ej.lpszMenuName, NULL
mov ej.lpszClassName, OFFSET NomeClasse invoke RegisterClassEx, ADDR ej gerenteJanela endp
Criando a janela Se a nossa classe foi aceita pelo sistema para registro, isto significa que possuímos um "alvará de construção". Podemos criar quantos objetos quisermos usando a classe registrada como modelo. Registra-se apenas uma vez, usa-se quantas vezes forem necessárias.
A função CreateWindowEx A função para criar uma janela de acordo com a classe que registramos é do grupo Ex, ou seja, CreateWindowEx (WNDCLASSEX -> RegisterClassEx -> CreateWindowEx). Seria CreateWindow caso tivéssemos usado uma estrutura WNDCLASS. HWND CreateWindowEx( DWORD dwExStyle, LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam );
Esta função pede um caminhão de parâmetros (12 ao todo, se você tiver o trabalho de contar). Então, vamos lá:
dwExStyle: Estilo extra da janela. Este é um parâmetro novo, adicionado ao CreateWindow antigo. Aqui podemos colocar os novos estilos das janelas do Windows 9x, NT e XP. Você pode especificar o estilo de janela comum em dwStyle porém, se você desejar estilos especiais, como janelas sempre no topo, você precisa especificá-los aqui. Usa-se NULL caso estilos extras não sejam requeridos. lpClassName: (Requerido) Endereço da string ASCIIZ (string terminada em zero) que contém o nome da classe que serve de modelo para esta janela. A classe pode ser uma que você tenha criado e registrado ou uma classe predefinida do Windows. lpWindowName: Endereço da string ASCIIZ que contém o nome da janela. O nome mostrado na barra de título da janela. Se este parâmetro for NULL, a barra de título ficará em branco.
dwStyle: Estilos da janela. Aqui você pode especificar a aparência da janela. Passar NULL também é aceito mas, neste caso, a janela não terá uma caixa de menu do sistema, botões minimizar-maximizar e botão fechar (a janela não teria muito uso e você teria que usar Alt+F4 para fechá-la). O estilo mais comum é o WS_OVERLAPPEDWINDOW. Um estilo de janela é apenas um bit de flag, portanto, você pode combinar diversos estilos com o operador "or" para obter a aparência desejada. O estilo WS_OVERLAPPEDWINDOW nada mais é do que a combinação dos estilos mais comuns obtido através deste método. X, Y: As coordenadas do canto superior esquerdo da janela. Normalmente, estes valores deveriam ser CW_USERDEFAULT, ou seja, você deixa o Windows decidir onde colocar a janela no desktop. nWidth, nHeight: A largura e a altura da janela em pixels. Aqui você também pode usar CW_USERDEFAULT e deixar o Windows escolher a largura e a altura apropriadas. hWndParent: O manipulador da janela-mãe (se existir). Este parâmetro indica ao Windows se esta janela é uma janela-filha (subordinada) ou algum outro tipo de janela e, se for, qual é a janela-mãe. Este relacionamento é apenas para uso interno do Windows. Se a janela-mãe é destruída, todas as janelas-filhas serão automaticamente destruídas. É realmente simples. Como no nosso exemplo há apenas uma janela, nós especificamos este parâmetro como NULL. hMenu: Um manipulador para o menu da janela. NULL se for para usar a classe menu já definida na classe da janela. Dê novamente uma olhada no membro lpszMenuName da estrutura WNDCLASSEX. Este membro especifica o menu *default* para a classe. Toda janela criada a partir desta classe terá o mesmo menu por default, a não ser que você especifique aqui em hMenu um menu que se sobreponha ao menu default - isto se chamda overriding. hMenu é na verdade um parâmetro com dois objetivos. Se a janela que está sendo criada é do tipo predefinido, como button (botão) ou edit box (caixa de edição), esta janela/controle não pode possuir um menu. Neste caso, o hMenu é usado como identificador (ID) do controle. O Windows distingue se hMenu é realmente um manipulador de menu ou um ID de controle olhando no parâmetro lpClassName. Se for o nome de uma classe predefinida, o hMenu é um ID de controle. Se não for, então é um manipulador do menu da janela. hInstance: O manipulador da instância para o módulo do programa que cria a janela. lpParam: Um ponteiro opcional para uma estrutura de dados passada para a janela. É usado por janelas MDI para passar os dados de CLIENTCREATESTRUCT. Normalmente este valor é NULL, significando que não há dados sendo passados via CreateWindow(). A janela pode obter o valor deste parâmetro através da chamada da função GetWindowLong.
Chamando a função É explicação que não acaba mais e tudo isso só para chamar uma funçãozinha! Para todos os parâmetros já temos os valores, exceto para o título da janela. Este vocês já tiram de letra: basta inicializar uma variável na seção .DATA. Também vamos precisar do valor de retorno da função CreateWindowEx, que é o manipulador da instância da janela que acabamos de criar. Sem este manipulador, ou seja, sem o número identificador desta janela recém criada, não teremos como acessá-la. Como vamos
precisar deste manipulador apenas no âmbito da função gerenteJanela, podemos declarar uma variável LOCAL para armazená-lo. ... .DATA NomeClasse db "JanelaNua",0 TituloJanela db "Janelinha NumaBoa",0 ... gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX LOCAL mJanela:HWND mov ej.cbSize, SIZEOF WNDCLASSEX mov ej.style, CS_HREDRAW or CS_VREDRAW mov ej.lpfnWndProc, OFFSET gerenteMensagem mov ej.cbClsExtra, NULL mov ej.cbWndExtra, NULL push mInst pop ej.hInstance invoke LoadIcon, NULL, IDI_WINLOGO mov ej.hIcon, eax mov ej.hIconSm, eax invoke LoadCursor, NULL, IDC_ARROW mov ej.hCursor, eax mov ej.hbrBackground, COLOR_WINDOW+1 mov ej.lpszMenuName, NULL mov ej.lpszClassName, OFFSET NomeClasse invoke RegisterClassEx, ADDR ej invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, mInst, NULL mov mJanela,eax gerenteJanela endp
Você deve estar pensando "até que enfim a janela está na tela"... ledo engano. A janela foi criada (a "casa foi construída"), está prontinha para uso, só que ninguém contou ao sistema que é para abrí-la ao público.
Pilotando a janela Planta da casa elaborada, planta registrada, casa construída... é hora de inaugurá-la. Vamos mostrar nossa janela no desktop com a função ShowWindow BOOL ShowWindow( HWND hWnd, // manipulador da janela int nCmdShow // modo de apresentação );
adicionando-a ao nosso código: gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX LOCAL mJanela:HWND ... invoke RegisterClassEx, ADDR ej invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, mInst, NULL mov mJanela,eax invoke ShowWindow, mJanela, SW_SHOWNORMAL gerenteJanela endp
Atualizando a janela Pode-se chamar UpdateWindow para refazer a pintura da área cliente da nossa janela. Esta função é útil quando se quer atualizar o conteúdo da área cliente. Esta chamada pode ser omitida, sem problemas... mas sempre é bom garantir. E, já que estamos garantindo algumas coisas, também vamos providenciar o retorno da função gerenteJanela para a área do procedimento chamador. O mnemônico ret garante esta volta. gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX LOCAL mJanela:HWND ... invoke RegisterClassEx, ADDR ej invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, mInst, NULL mov mJanela,eax invoke ShowWindow, mJanela, SW_SHOWNORMAL invoke UpdateWindow, mJanela ret gerenteJanela endp
Estrutura das mensagens Se interrompermos nosso código neste ponto, teremos uma surpresa: executando o programa, nossa linda janelinha é mostrada no desktop uma fração de segundo e desaparece. Faça o teste: Clique no item de menu do QEditor [Project / Assemble & Link] para criar o executável e depois em [Project / Run Program]. O que falta é um trechinho de código cuja função é a de manter a janela no desktop e, o
que é mais importante, fazer com que ela saiba o que acontece com ela. Imagine o seguinte: quem controla todos os eventos que ocorrem com esta janela é o Windows. Aliás, o Windows tem uma espécie de central que controla todos os eventos referentes a todas as janelas. É como se fosse uma agência de correio onde cada instância de janela possui sua caixa postal. Quando o gerente da janela solicita sua correspondência, a agência lhe envia um malote com a mensagem recebida - uma atrás da outra e sempre a primeira da fila - inclusive com o número da mensagem e a hora! O gerente da janela confere o recebimento e envia a mensagem para o gerente de mensagens. O gerente de mensagens da janela pode responder a mensagem a seu modo (ao nosso modo ) ou pode devolvê-la para o remetente (o Windows) e deixar que ele processe a mensagem de acordo com as regras do sistema (procedimento padrão). Observação: no sistema Windows, o gerente de mensagens é conhecido como window procedure. Ainda bem que a "agência de correio" cuida da correspondência da nossa janela. Só precisamos contratar os gerentes e fornecer o malote. O malote traz diversas informações que podem ser agrupadas, portanto o ideal é usar uma estrutura. A estrutura utilizada como "malote" é a MSG: typedef struct tagMSG { // msg HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG;
Os membros que constituem a estrutura MSG são:
hwnd: Identifica a janela cujo procedimento de janela (nosso gerenteMensagem) recebe a mensagem. message: Especifica o número da mensagem. wParam: Informação adicional sobre a mensagem. O sentido exato depende do valor do membro mensagem. lParam: Informação adicional sobre a mensagem. O sentido exato depende do valor do membro mensagem. time: A hora que a mensagem foi enviada. pt: A posição do cursor, em coordenadas de janela, quando a mensagem foi enviada.
Pois bem, então vamos criar o "malote" da nossa janela: gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX LOCAL mJanela:HWND LOCAL malote:MSG ... invoke RegisterClassEx, ADDR ej
invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, mInst, NULL mov mJanela,eax invoke ShowWindow, mJanela, SW_SHOWNORMAL invoke UpdateWindow, mJanela ret gerenteJanela endp
Solicitando e enviando mensagens O gerente da janela solicita o envio de um "malote" com uma mensagem através da função GetMessage: BOOL GetMessage( LPMSG lpMsg, // endereço da estrutura com a mensagem HWND hWnd, // manipulador da janela UINT wMsgFilterMin, // primeira messagem UINT wMsgFilterMax // última messagem );
Existem algumas mensagens que chegam à "agência de correio" com características especiais. São como cartas registradas ou SEDEX: não podem ser entregues via caixa postal ou malote. Se usarmos o manipulador da janela (no exemplo, mJanela) como parâmetro hWnd da função GetMessage, apenas as "cartas normais" serão entregues. Se quisermos obter todas as mensagens, as "normais" E as "especiais", passamos este parâmetro como NULL. Se não quisermos selecionar nossa correspondência (não queremos filtrar mensagens), passamos os parâmetros wMsgFilterMin e wMsgFilterMax como zero e a "agência" entregará TODAS as mensagens. Se a função mandar um "malote" ao gerente de mensagens contendo a mensagem WM_QUIT, o valor de retorno será zero; caso contrário, o valor de retorno será diferente de zero. Se ocorrer um erro, o valor de retorno será -1 (por exemplo, quando o manipulador da janela não for válido). Como já foi dito anteriormente, o Windows controla todos os eventos referentes a todas as janelas, inclusive os relativos à janela que acabamos de criar: a janela foi ativada, o cursor do mouse passou sobre ela, clique do mouse, tecla pressionada, janela minimizada, maximizada... tudo e mais alguma coisa gera uma mensagem. Dê uma olhada na referência da API do Windows e procure por WM_ (vem de Windows Message). Existe uma quantidade enorme de tipos de mensagens. Caso a mensagem seja referente a uma tecla que foi pressionada, ela vem com um código de varredura do teclado. É muito chato trabalhar com estes códigos - mais fácil é trabalhar com o ASCII da tecla. Existe uma função que transforma uma mensagem WM_KEYDOWN (código de varredura) em uma mensagem WM_CHAR (código ASCII). É a função TranslateMessage:
BOOL TranslateMessage( CONST MSG *lpMsg // endereço da estrutura com a mensagem (o "malote") );
Para o gerente da janela mandar a mensagem ao gerente de mensagens, usamos a função DispatchMessage: LONG DispatchMessage( CONST MSG *lpmsg // endereço da estrutura com a mensagem (o "malote") );
Loop infinito Bem, então vamos ao trabalho. Queremos que nosso gerente de janela, depois de tudo que já fez, fique o tempo todo solicitando, conferindo e mandando suas mensagens para o gerente de mensagens. Para isto faremos uso de macros de pseudo alto nível do MASM: .WHILE, .BREAK e .IF gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX LOCAL mJanela:HWND LOCAL malote:MSG ... invoke RegisterClassEx, ADDR ej invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, mInst, NULL mov mJanela,eax invoke ShowWindow, mJanela, SW_SHOWNORMAL invoke UpdateWindow, mJanela .WHILE TRUE invoke GetMessage, ADDR malote, NULL, 0, 0 .BREAK .IF (eax < 1) invoke TranslateMessage, ADDR malote invoke DispatchMessage, ADDR malote .ENDW ret gerenteJanela endp
Trocando em miúdos, este trecho de código diz o seguinte: enquanto (.WHILE) verdadeiro (TRUE) chame MandeMessagem, pelo malote, mensagens normais e especiais, sem filtro, sem filtro páre (.BREAK) se (.IF) o valor de retorno for menor que 1 chame TraduzaMensagem, do malote chame DespacheMensagem, do malote fim do enquanto (.ENDW)
Este loop infinito só é interrompido quando o valor de retorno de GetMessage for 0 ou 1 (ou seja, menor que 1). Quando é 0, a mensagem recebida foi WM_QUIT e o programa deve ser encerrado. O valor -1 indica erro e o melhor é... também terminar ao invés de dar pau. Saindo do loop, o gerente da janela ainda deve realizar uma tarefa antes de devolver o controle ao corpo principal do código: deve armazenar o código de saída em eax para devolvê-lo ao Windows. Até o presente momento, o Windows não faz uso deste valor de retorno, mas é melhor agir com segurança e jogar conforme a regra: gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD ... .WHILE TRUE invoke GetMessage, ADDR malote, NULL, 0, 0 .BREAK .IF (eax < 1) invoke TranslateMessage, ADDR malote invoke DispatchMessage, ADDR malote .ENDW mov eax, malote.wParam ret gerenteJanela endp
Agora o gerente da janela está com todo o seu esquema de trabalho definido. Uma de suas funções é enviar as mensagens recebidas para o gerente de mensagens para que sejam processadas. Então, vamos para a última função...
Processando mensagens recebidas Agora é a vez do nosso window procedure - o gerente de mensagens. Você pode dar a ele o nome que quiser - não precisa chamá-lo de WindowProc: LRESULT CALLBACK WindowProc( HWND hwnd, // manipulador da janela UINT uMsg, // identificador da mensagem WPARAM wParam, LPARAM lParam );
O primeiro parâmetro, hWnd, é o manipulador da janela para a qual a mensagem é destinada. uMsg é a mensagem. Observe que uMsg NÃO é uma estrutura MSG, é apenas um número identificador. O Windows define centenas de mensagens, a maioria sem maior interesse para o nosso programa, e irá enviar uma mensagem apropriada apenas quando ocorrer um fato relevante para a nossa janela. wParam e lParam são apenas parâmetros extras usados por algumas mensagens que enviam dados acessórios. No nosso exemplo, vamos chamar o WindowProc de gerenteMensagem: gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ...
gerenteMensagem endp
O gerente de mensagens precisa checar cada mensagem recebida do Windows para verificar se é de interesse. Se for de interesse, deve dar a resposta adequada; se não for, PRECISA chamar DefWindowProc, passando todos os parâmetros que recebeu, para que o processamento default seja efetuado. Esta DefWindowProc é uma função da API que processa as todas as mensagens que não são do interesse do seu programa. gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret gerenteMensagem endp
Encerrando o programa A única mensagem que SEMPRE PRECISA ser respondida é a WM_DESTROY. Esta mensagem é enviada toda vez a janela for fechada - é o sinal de final de expediente. No momento em que esta mensagem é recebida, a janela já foi removida da tela. Esta é apenas uma notificação de que a janela foi destruída e que os "gerentes" devem encerrar o expediente e se liberar do controle do Windows. Ainda há tempo de fazer alguma "ordem no escritório" mas, quando se chegou neste ponto, não há outra opção a não ser terminar. Se você quiser ter uma chance de impedir que o usuário feche a janela, você deve processar a mensagem WM_CLOSE. gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg==WM_DESTROY invoke PostQuitMessage, NULL xor eax,eax ret .ENDIF invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret gerenteMensagem endp
Bem, voltando à WM_DESTROY... após a eventual "ordem no escritório", o gerente de mensagens precisa chamar PostQuitMessage (a qual remeterá uma mensagem WM_QUIT), zerar o valor do registrador EAX com xor eax,eax ou mov eax,0 e retornar (ret) para o gerente da janela. O gerente da janela vai chamar GetMessage pela última vez, obtendo a mensagem WM_QUIT que acabou de ser enviada pelo gerente de mensagens. GetMessage retorna o valor zero em eax quando recebe WM_QUIT o que, por seu lado, encerra o loop infinito. O gerente da janela se despede enviando o malote.wParam para o Windows e o fluxo do programa volta para o corpo principal para executar a saída do processo chamando ExitProcess.
Código fonte completo
.386 .MODEL FLAT,STDCALL option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib gerenteJanela proto :DWORD, :DWORD, :DWORD, :DWORD .DATA? mInstancia DWORD ? linhaComando DWORD ? .DATA NomeClasse db "JanelaNua",0 TituloJanela db "Janelinha NumaBoa",0 .CODE inicio: invoke GetModuleHandle, NULL mov mInstancia, eax invoke GetCommandLine mov linhaComando, eax invoke gerenteJanela, mInstancia, NULL, linhaComando, SW_SHOWDEFAULT invoke ExitProcess,0 gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX LOCAL mJanela:HWND LOCAL malote:MSG mov ej.cbSize, SIZEOF WUNDCLASSEX mov ej.style, CS_HREDRAW or CS_VREDRAW mov ej.lpfnWdnProc, OFFSET gerenteMensagem mov ej.cbClsExtra, NULL mov ej.cbWndExtra, NULL push mInst pop ej.hInstance invoke LoadIcon, NULL, IDI_WINLOGO mov ej.hIcon, eax mov ej.hIconSm, eax invoke LoadCursor, NULL, IDC_ARROW mov ej.hCursor, eax mov ej.hbrBackground, COLOR_WINDOW+1 mov ej.lpszMenuName, NULL mov ej.lpszClassName, OFFSET NomeClasse invoke RegisterClassEx, ADDR ej invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, mInst, NULL mov mJanela,eax invoke ShowWindow, mJanela, SW_SHOWNORMAL invoke UpdateWindow, mJanela .WHILE TRUE invoke GetMessage, ADDR malote, NULL, 0, 0 .BREAK .IF (eax < 1) invoke TranslateMessage, ADDR malote invoke DispatchMessage, ADDR malote .ENDW mov eax, malote.wParam ret gerenteJanela endp gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg==WM_DESTROY invoke PostQuitMessage, NULL xor eax,eax ret .ENDIF invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret gerenteMensagem endp end inicio
Conferindo o trabalho do MASM O QEditor não só é capaz de assemblar um código fonte, como também de dessassemblar um executável. Esta opção se encontra no menu [Tools / Dis-assemble EXE file]. Experimente usá-la com o texto deste programa ainda no editor. O QEditor abre uma nova instância dele mesmo e apresentará o seguinte: :\masm32\ICZTUTES\TUTE03\WIN1.exe
.EXE size (bytes) Minimum load size (bytes) Overlay number Initial CS: IP Initial SS:SP Minimum allocation (para) Maximum allocation (para) Header size (para) Relocation table offset Relocation entries
(hex)
490 450 0 0000:0000 0000:00B8 0 FFFF 4 40 0
(dec)
1168 1104 0 184 0 65535 4 64 0
Portable Executable starts at b0 Signature 00004550 (PE) Machine 014C (Intel 386) Sections 0003 Time Date Stamp 3C4CEBF4 Tue Jan 22 01:35:00 2002
Symbol Table 00000000 Number of Symbols 00000000 Optional header size 00E0 Characteristics 010F Relocation information stripped Executable Image Line numbers stripped Local symbols stripped 32 bit word machine Magic 010B Linker Version Size of Code 00000200 Size of Initialized Data 00000400 Size of Uninitialized Data 00000000 Address of Entry Point 00001000 Base of Code 00001000 Base of Data 00002000 Image Base 00400000 Section Alignment 00001000 File Alignment 00000200 Operating System Version Image Version Subsystem Version reserved 00000000 Image Size 00004000 Header Size 00000400 Checksum 00000000 Subsystem 0002 (Windows) DLL Characteristics 0000 Size Of Stack Reserve 00100000 Size Of Stack Commit 00001000 Size Of Heap Reserve 00100000 Size Of Heap Commit 00001000 Loader Flags 00000000 Number of Directories 00000010 Directory Name
VirtAddr
Export Import Resource Exception Security Base Relocation Debug Decription/Architecture Machine Value (MIPS GP) Thread Storage Load Configuration Bound Import Import Address Table Delay Import COM Runtime Descriptor (reserved) Section Table
00000000 00002040 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00002000 00000000 00000000 00000000
5.12
4.00 0.00 4.00
VirtSize
00000000 0000003C 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000040 00000000 00000000 00000000
01 .text
Virtual Address Virtual Size Raw Data Offset Raw Data Size Relocation Offset Relocation Count Line Number Offset Line Number Count Characteristics
00001000 00000196 00000400 00000200 00000000 0000 00000000 0000 60000020 Code Executable Readable
02 .rdata
Virtual Address Virtual Size Raw Data Offset Raw Data Size Relocation Offset Relocation Count Line Number Offset Line Number Count Characteristics
00002000 000001C2 00000600 00000200 00000000 0000 00000000 0000 40000040 Initialized Data Readable
03 .data
Virtual Address Virtual Size Raw Data Offset Raw Data Size Relocation Offset Relocation Count Line Number Offset Line Number Count Characteristics
00003000 00000024 00000800 00000200 00000000 0000 00000000 0000 C0000040 Initialized Data Readable Writeable
Imp Addr
Hint
Import Name from USER32.dll - Not Bound
00002010 00002014 00002018 0000201C 00002020 00002024 00002028 0000202C 00002030 00002034 00002038
19B 1DD 128 94 27D 28B 197 83 58 1EF 265
LoadIconA PostQuitMessage GetMessageA DispatchMessageA TranslateMessage UpdateWindow LoadCursorA DefWindowProcA CreateWindowExA RegisterClassExA ShowWindow
Imp Addr
Hint
Import Name from KERNEL32.dll - Not Bound
00002000 00002004 00002008
111 B6 75
GetModuleHandleA GetCommandLineA ExitProcess
IAT Entry
00000000: 000021A0 0000218E - 00002180 00000000 - 00002110 0000211C 00000018: 000020F4 000020E0 - 00002150 00002164 - 00002102 000020CE 00000030: 000020BC 0000212E - 00002142 00000000 Disassembly
00401000 start: 00401000 6A00 00401002 E889010000 00401007 A31C304000 0040100C E879010000 00401011 A320304000 00401016 6A0A 00401018 FF3520304000 0040101E 6A00 00401020 FF351C304000 00401026 E806000000 0040102B 50 0040102C E853010000
push 0 call fn_00401190 mov [40301Ch],eax call fn_0040118A mov [403020h],eax push 0Ah push dword ptr [403020h] push 0 push dword ptr [40301Ch] call fn_00401031 push eax call fn_00401184
00401031 fn_00401031: 00401031 55 00401032 8BEC 00401034 83C4B0 00401037 C745D030000000 0040103E C745D403000000 00401045 C745D816114000 0040104C C745DC00000000 00401053 C745E000000000 0040105A FF7508 0040105D 8F45E4 00401060 68057F0000 00401065 6A00 00401067 E8F4000000 0040106C 8945E8 0040106F 8945FC 00401072 68007F0000 00401077 6A00 00401079 E8DC000000 0040107E 8945EC 00401081 C745F006000000 00401088 C745F400000000 0040108F C745F800304000 00401096 8D45D0 00401099 50 0040109A E8CD000000 0040109F 6A00 004010A1 FF7508 004010A4 6A00 004010A6 6A00 004010A8 6800000080 004010AD 6800000080 004010B2 6800000080 004010B7 6800000080
; o nosso gerenteJanela push ebp mov ebp,esp add esp,0FFFFFFB0h mov dword ptr [ebp-30h],30h mov dword ptr [ebp-2Ch],3 mov dword ptr [ebp-28h],401116h mov dword ptr [ebp-24h],0 mov dword ptr [ebp-20h],0 push dword ptr [ebp+8] pop [ebp-1Ch] push 7F05h push 0 call fn_00401160 mov [ebp-18h],eax mov [ebp-4],eax push 7F00h push 0 call fn_0040115A mov [ebp-14h],eax mov dword ptr [ebp-10h],6 mov dword ptr [ebp-0Ch],0 mov dword ptr [ebp-8],403000h lea eax,[ebp-30h] push eax call fn_0040116C ; registra a classe push 0 push dword ptr [ebp+8] push 0 push 0 push 80000000h push 80000000h push 80000000h push 80000000h
004010BC 004010C1 004010C6 004010CB 004010CD 004010D2 004010D5 004010D7 004010DA 004010DF 004010E2
680000CF00 680A304000 6800304000 6A00 E870000000 8945B0 6A01 FF75B0 E893000000 FF75B0 E897000000
push 0CF0000h push 40300Ah push 403000h push 0 call fn_00401142 ; cria a janela mov [ebp-50h],eax push 1 push dword ptr [ebp-50h] call fn_00401172 ; mostra a janela push dword ptr [ebp-50h] call fn_0040117E
004010E7 loc_004010E7: 004010E7 6A00 004010E9 6A00 004010EB 6A00 004010ED 8D45B4 004010F0 50 004010F1 E85E000000 004010F6 83F801 004010F9 7214 004010FB 8D45B4 004010FE 50 004010FF E874000000 00401104 8D45B4 00401107 50 00401108 E841000000 0040110D EBD8
; o loop infinito do gerenteJanela push 0 push 0 push 0 lea eax,[ebp-4Ch] push eax call fn_00401154 cmp eax,1 jb loc_0040110F lea eax,[ebp-4Ch] push eax call fn_00401178 lea eax,[ebp-4Ch] push eax call fn_0040114E ; chama o gerenteMensagem jmp loc_004010E7
0040110F loc_0040110F: 0040110F 8B45BC 00401112 C9 00401113 C21000 00401116 55 00401117 8BEC 00401119 837D0C02 0040111D 750D 0040111F 6A00 00401121 E840000000 00401126 33C0 00401128 C9 00401129 C21000
; o nosso gerenteMensagem mov eax,[ebp-44h] leave ret 10h push ebp mov ebp,esp cmp dword ptr [ebp+0Ch],2 jnz loc_0040112C push 0 call fn_00401166 xor eax,eax leave ret 10h
0040112C loc_0040112C: 0040112C FF7514 0040112F FF7510 00401132 FF750C 00401135 FF7508 00401138 E80B000000 0040113D C9 0040113E C21000 00401141 CC
push dword ptr [ebp+14h] push dword ptr [ebp+10h] push dword ptr [ebp+0Ch] push dword ptr [ebp+8] call fn_00401148 leave ret 10h int 3
00401142 fn_00401142: 00401142 FF2530204000
jmp dword ptr [CreateWindowExA]
00401148 00401148 0040114E 0040114E 00401154 00401154 0040115A 0040115A 00401160 00401160 00401166 00401166 0040116C 0040116C 00401172 00401172 00401178 00401178 0040117E 0040117E 00401184 00401184 0040118A 0040118A 00401190 00401190
fn_00401148: FF252C204000 fn_0040114E: FF251C204000 fn_00401154: FF2518204000 fn_0040115A: FF2528204000 fn_00401160: FF2510204000 fn_00401166: FF2514204000 fn_0040116C: FF2534204000 fn_00401172: FF2538204000 fn_00401178: FF2520204000 fn_0040117E: FF2524204000 fn_00401184: FF2508204000 fn_0040118A: FF2504204000 fn_00401190: FF2500204000
jmp dword ptr [DefWindowProcA] jmp dword ptr [DispatchMessageA] jmp dword ptr [GetMessageA] jmp dword ptr [LoadCursorA] jmp dword ptr [LoadIconA] jmp dword ptr [PostQuitMessage] jmp dword ptr [RegisterClassExA] jmp dword ptr [ShowWindow] jmp dword ptr [TranslateMessage] jmp dword ptr [UpdateWindow] jmp dword ptr [ExitProcess] jmp dword ptr [GetCommandLineA] jmp dword ptr [GetModuleHandleA]
Download Você pode fazer o download de tutNB03.zip que contém o texto deste tutorial, além do código fonte e do executável ou ir para a seção Downloads/Tutoriais/Assembly Numaboa onde você encontra este e outros zips de tutoriais.
Índice do Artigo Usando recursos (masm) Programa teste Criando a janela Resultado final Todas as páginas Depois do tutorial Janelas somos felizes possuidores de uma classe que nos
permite criar janelas com a nossa grife. A janela que produzimos no tutorial anterior estava peladinha da silva, onde muitos recursos que costumam acompanhar janelas estão faltando. Este tutorial tem justamente este propósito: discutir os RECURSOS. Como exemplo, vamos colocar um bitmap numa janela.
O que são recursos Recursos são dados binários que um compilador de recursos ou o programador adicionam ao arquivo executável de um aplicativo. Recursos podem ser do tipo padrão ou do tipo definido. Os dados de um recurso padrão descrevem ícones, cursores, menus, caixas de diálogo, gráficos do tipo .bmp e .emf, fontes, tabelas de teclas de atalho, tabela de mensagens, tabela de strings ou versão. Um recurso definido pelo aplicativo, também chamado de recurso personalizado (custom resource), pode conter quaisquer dados requeridos por um aplicativo específico. Os recursos são descritos em arquivos texto próprios, chamados arquivos de recurso, geralmente com a extensão .rc. Estes arquivos precisam ser compilados (no menu do MASM [Project / Compile Resource File]) e, depois de compilados, podem ser combinados com o arquivo de código fonte durante o estágio de link. O produto final é um arquivo executável que contém instruções e recursos. Podemos usar qualquer editor de texto para escrever arquivos de recursos. O texto é composto por frases que descrevem a aparência e os atributos dos recursos desejados para um determinado programa. Os recursos possuem uma linguagem própria, a Resource Script Language. Conhecendo um mínimo desta linguagem é o suficiente para podermos trabalhar (o help do Resource Workshop da Borland é uma referência muito boa). Existem editores de recursos que facilitam muito o trabalho por oferecerem uma plataforma de trabalho visual. Editores de recursos geralmente estão incluídos nos pacotes de compiladores como Visual C++, Borland C++, etc. Você pode usar o excelente XN Resource Editor, o ResourceStudio da Symantec ou o Resource Compiler. Estes você encontra na seção de Downloads/Informática/Compiladores.
Criando um arquivo de recursos Use o programa gráfico da sua preferência para criar um gráfico ou escolha um da sua preferência. Salve-o ou transfira-o para o diretório de trabalho deste tutorial. O arquivo de recursos deve ficar no mesmo diretório do arquivo com o código fonte. O gráfico que preparei para este tutorial recebeu o nome de "bits.bmp" e é o que se vê abaixo:
A diretiva para indicar um recurso bitmap é a seguinte: IDentificador BITMAP [tipo de carregamento] [opção de memória] NomeDoArquivo
Identificador é o ID do recurso, que é um número. O ID será usado quando quisermos usar este recurso no programa (Obrigatório). BITMAP é uma palavra chave para indicar o tipo de recurso (Obrigatório). Tipo de carregamento é opcional. Indica se queremos que o bitmap seja previamente carregado (PRELOAD) ou apenas seja carregado quando solicitado (LOADONCALL). O default é LOADONCALL. Opção de memória indica como o recurso deve ser carregado na memória. Pode ser descartável para livrar espaço de memória (DISCARDABLE), ficar fixo num endereço de memória (FIXED), ser modificado após o carregamento (IMPURE), pode ser deslocado na memória (MOVEABLE), precisa permanecer na memória (NONDISCARDABLE), não é modificado após o carregamento (PURE). O default é MOVEABLE e DISCARDABLE. NomeDoArquivo é o nome do bitmap.
Podemos resumir nosso arquivo RSRC.RC numa linha que contenha apenas o ID, a palavra chave BITMAP e o nome do arquivo. O restante deixamos como default. Abra um editor de texto (pode ser o GUN do MASM que você encontra em [Tools / TheGUN Text Editor]) e digite a linha abaixo: 760 BITMAP "bits.bmp"
Você pode usar qualquer número para o ID, contanto que não seja o mesmo usado por outro recurso. Salve o arquivo com o nome de "RSRC.RC" no mesmo diretório onde estará o código fonte do nosso programa teste. Por enquanto é só.
O programa teste Vamos criar uma janela que conterá o bitmap do recurso, portanto podemos partir do modelo "Janelas" que criamos no tutorial anterior. .386 .MODEL FLAT,STDCALL
option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib gerenteJanela proto :DWORD, :DWORD, :DWORD, :DWORD .DATA? mInstancia DWORD ? linhaComando DWORD ? .DATA NomeClasse db "JanelaNua",0 TituloJanela db "Janelinha NumaBoa",0 .CODE inicio: invoke GetModuleHandle, NULL mov mInstancia, eax invoke GetCommandLine mov linhaComando, eax invoke gerenteJanela, mInstancia, NULL, linhaComando, SW_SHOWDEFAULT invoke ExitProcess,0 gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD LOCAL ej:WNDCLASSEX LOCAL mJanela:HWND LOCAL malote:MSG mov ej.cbSize, SIZEOF WNDCLASSEX mov ej.style, CS_HREDRAW or CS_VREDRAW mov ej.lpfnWndProc, OFFSET gerenteMensagem mov ej.cbClsExtra, NULL mov ej.cbWndExtra, NULL push mInst pop ej.hInstance invoke LoadIcon, NULL, IDI_WINLOGO mov ej.hIcon, eax mov ej.hIconSm, eax invoke LoadCursor, NULL, IDC_ARROW mov ej.hCursor, eax mov ej.hbrBackground, COLOR_WINDOW+1 mov ej.lpszMenuName, NULL mov ej.lpszClassName, OFFSET NomeClasse invoke RegisterClassEx, ADDR ej invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, mInst, NULL mov mJanela,eax
invoke ShowWindow, mJanela, SW_SHOWNORMAL invoke UpdateWindow, mJanela .WHILE TRUE invoke GetMessage, ADDR malote, NULL, 0, 0 .BREAK .IF (eax < 1) invoke TranslateMessage, ADDR malote invoke DispatchMessage, ADDR malote .ENDW mov eax, malote.wParam ret gerenteJanela endp gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg == WM_CREATE .ELSEIF uMsg == WM_SIZE .ELSEIF uMsg == WM_PAINT .ELSEIF uMsg == WM_COMMAND .ELSEIF uMsg == WM_CLOSE .ELSEIF uMsg==WM_DESTROY invoke PostQuitMessage, NULL xor eax,eax ret .ENDIF invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret gerenteMensagem endp end inicio
Vamos mudar o título da janela para "NumaBoa com Recursos" só para identificá-la melhor: .DATA NomeClasse db "JanelaNua",0 TituloJanela db "NumaBoa com Recursos",0
Como queremos que o gráfico seja mostrado assim que a janela aparecer na tela, precisamos gerenciar a mensagem WM_CREATE que é enviada ao gerenteMensagem assim que a janela é criada. Primeiramente vamos criar uma área dentro da janela principal onde o bitmap deve ser mostrado. Essa área especial nada mais é que um controle, ou seja, uma janela especial. Já sabemos que, para criar uma janela/controle, precisamos de uma classe registrada que servirá como modelo. No Windows existem classes predefinidas para controles: BUTTON, COMBOBOX, EDIT, LISTBOX, SCROLLBAR e STATIC. Usaremos a STATIC, por que não esperamos adicionar nenhuma funcionalidade ao nosso bitmap. A primeira provicência, então, é inicializar
uma variável com o nome da classe na seção .DATA: .DATA NomeClasse db "JanelaNua",0 TituloJanela db "Janelinha NumaBoa",0 ClasseAreaBMP db "STATIC",0
Criando a janela Como vamos precisar da função CreateWindowEx, não custa dar uma recapitulada: HWND CreateWindowEx( DWORD dwExStyle, // estilo especial de janela LPCTSTR lpClassName, // ponteiro para a classe registrada LPCTSTR lpWindowName, // pointeiro para o nome da janela: NULL DWORD dwStyle, // estilo da janela int x, // posição horizontal da janela int y, // posição vertical da janela int nWidth, // largura da janela int nHeight, // altura da janela HWND hWndParent, // manipulador da janela-mãe ou proprietário da janela HMENU hMenu, // manipulador do menu ou identificador da janela-filha HINSTANCE hInstance, // manipulador da instância do aplicativo LPVOID lpParam // ponteiro para dados de criação da janela );
dwExStyle: não usaremos um estilo especial - NULL. lpClassName: o nome da classe está na variável ClasseAreaBMP - seu endereço será ADDR ClasseAreaBMP. lpWindowName: a janela não precisa de título - usaremos NULL. dwStyle: a composição do estilo será "Estilo Janela (WS) Filha" + "Estilo Janela (WS) Visível" + "Controle Estático (SS) com Bitmap" que é indicado com WS_CHILD or WS_VISIBLE or SS_BITMAP. x e y: a posição horizontal e vertical do canto superior esquerdo da janela do controle dentro da janela-mãe - indicamos ambos como 20 pixels. nWidth e nHeight: a largura e a altura da janela em pixels - indicamos temporariamente como 10. O porque do temporário será explicado logo adiante (4.4. Preparando a pintura). hWndParent: o manipulador da janela-mãe - no nosso exemplo é o parâmetro hWnd do procedimento gerenteMensagem. hMenu: não existe menu no controle, portanto NULL. hInstance: o manipulador do módulo, mInstancia. lpParam: não há dados na criação da janela - NULL.
Tudo em riba para podermos incluir a chamada à função que criará o controle com a área que deve conter o bitmap: gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg == WM_CREATE invoke CreateWindowEx, NULL, ADDR ClasseAreaBMP, NULL, WS_CHILD or WS_VISIBLE or SS_BITMAP, 20, 20, 10, 10, hWnd, NULL, mInstancia, NULL ... .ELSEIF ...
A função CreateWindowEx retorna o valor do manipulador da janela em EAX. Precisaremos deste valor logo adiante, portanto, criaremos uma variável local e a inicializamos com o valor de retorno: gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL mAreaBMP: DWORD .IF uMsg == WM_CREATE invoke CreateWindowEx, NULL, ADDR AreaBitmap, NULL, WS_CHILD or WS_VISIBLE or SS_BITMAP, 20, 20, 10, 10, hWnd, NULL, mInstancia, NULL mov mAreaBMP, eax ... .ELSEIF ...
Nosso bitmap está nos recursos e tem o identificador 760. Agora precisamos obter um manipulador para o bitmap para que possamos acessá-lo. Usamos a função LoadBitmap: HBITMAP LoadBitmap( HINSTANCE hInstance, // manipulador da instância do aplicativo LPCTSTR lpBitmapName // endereço do nome do recurso bitmap );
Da mesma forma que acima, precisamos de uma variável local que receba o manipulador: gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL mAreaBMP: DWORD LOCAL mBMP: DWORD .IF uMsg == WM_CREATE invoke CreateWindowEx, NULL, ADDR AreaBitmap, NULL, WS_CHILD or WS_VISIBLE or SS_BITMAP, 20, 20, 10, 10, hWnd, NULL, mInstancia, NULL mov mAreaBMP, eax invoke LoadBitmap, mInstancia, 760 mov mBMP, eax ... .ELSEIF ...
O sistema possui um "pintor" de plantão que pode ser acionado sempre que necessário. Basta enviar uma mensagem com os dados necessários para que ele possa trabalhar. A função SendMessage envia a mensagem especificada para uma ou mais janelas. Esta função chama o gerente de mensagens do(s) destinatário(s) e não retorna enquanto o
pedido não for integralmente realizado. LRESULT SendMessage( HWND hWnd, // manipulador da janela destino UINT Msg, // mensagem a ser enviada WPARAM wParam, // primeiro parâmetro da mensagem LPARAM lParam // segundo parâmetro da mensagem );
hWnd: nossa janela destino é a janela do controle com a área que foi preparada para receber o bitmap - mAreaBMP. Msg: a mensagem é dirigida a um controle estático (STatic control Message), ou seja, do tipo STM_ alguma coisa. As mensagens que existem são STM_GETICON, STM_SETICON, STM_GETIMAGE e STM_SETIMAGE. Obviamente é da STM_SETIMAGE que estamos precisando. wParam: o primeiro parâmetro de uma mensagem do tipo STM_SETIMAGE refere-se ao tipo de imagem e pode ser IMAGE_BITMAP, IMAGE_CURSOR, IMAGE_ENHMETAFILE e IMAGE_ICON. O tipo da nossa imagem é IMAGE_BITMAP. lParam: o segundo parâmetro da mensagem STM_SETIMAGE refere-se ao manipulador da imagem. No nosso caso, mBMP.
Vamos enviar nossa mensagem com: gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL mAreaBMP: DWORD LOCAL mBMP: DWORD .IF uMsg == WM_CREATE invoke CreateWindowEx, NULL, ADDR AreaBitmap, NULL, WS_CHILD or WS_VISIBLE or SS_BITMAP, 20, 20, 10, 10, hWnd, NULL, mInstancia, NULL mov mAreaBMP, eax invoke LoadBitmap, mInstancia, 760 mov mBMP, eax invoke SendMessage, mAreaBMP, STM_SETIMAGE, IMAGE_BITMAP, mBMP .ELSEIF ...
Você ainda se lembra de que dimensionamos nosso controle para ter 10 x 10 pixels? Nosso gráfico tem 255 x 185 pixels. Vai faltar pixel para abrigar o gráfico inteiro problema do "pintor" de plantão. Como enviamos as coordenadas da posição do controle estático, o "pintor" vai transferindo a sequência de pixels que ele encontrar nos recursos e "estica" o controle para fazer espaço para o gráfico. Tão simples assim... mas precisamos saber disso para que o gráfico não avance sobre outros controles desformatando nosso layout.
Finalmentes Neste exemplo, o resultado final será este:
Se você quiser fazer o download do texto, do código fonte e do executável deste tutorial, procure em Downloads / Tutoriais / Assembly Numaboa clicando no menu à direita. Espero que tenham gostado e, como sempre Grande abraço da vó Vicki
Quando se escreve código para Windows, é comum manusear dados em bloco para alguns requisitos de codificação. O método mais comum é usar uma estrutura para agrupar os dados de forma que possam ser endereçados como uma unidade.
Estruturas Uma estrutura é constituída por membros que têm um tamanho fixo para armazenar os dados. Na estrutura RECT, muito utilizada, existem quatro membros de tamanho DWORD. RECT STRUCT left DWORD ? top DWORD ? right DWORD ? bottom DWORD ? RECT ENDS
A notação para cada membro é: nome do membro, tamanho do dado e especificador. Na
maior parte das vezes o especificador é um ponto de interrogação (?), significando que o membro não foi inicializado com um valor. A estrutura é colocada na memória como uma sequência de membros. No caso da estrutura RECT, ela é escrita na memória como uma sequência de quatro membros de tamanho DWORD. Os membros de uma estrutura podem ser preenchidos de várias maneiras diferentes, dependendo da modo como a estrutura foi originalmente definida. Se ela tiver sido alocada na seção .DATA, eles podem ser inicializados com valores predefinidos. Se tiver sido alocada na pilha, como uma variável local de um procedimento, os valores precisam ser inseridos nesta estrutura através de codificação. Por exemplo: LOCAL Rct:RECT ; código
mov mov mov mov
Rct.left, 1 Rct.top, 2 Rct.right, 3 Rct.bottom, 4
É preciso salientar que um membro de uma estrutura é um operando de memória, o que significa que você não pode transferir diretamente outro operando de memória para ele. É preciso usar um registrador para copiá-lo ou usar os mnemônicos de pilha push/pop. Numa chamada com a diretiva invoke podemos nos referir à estrutura preenchida como uma unidade com ADDR Rct. Se uma chamada de API necessitar do endereço de uma estrutura, você deve preencher a estrutura com os valores requeridos e depois chamar a API de acordo com a seguinte sintaxe: invoke chamadaAPI, parametro1, parametro2, ADDR Rct
Se você escrever um procedimento para o qual você queira passar os valores de uma estrutura, pode passar esta estrutura usando os tipos de dados da estrutura no procedimento. MeuProc proc par1:DWORD, par2:DWORD, MeuRect:RECT mov eax, MeuRect.left ; copiar o primeiro membro para EAX
No procedimento que recebe RECT como parâmetro, cada um dos membros pode ser acessado através do nome. Você chama o procedimento da seguinte forma: invoke MeuProc, par1, par2, Rct
Estruturas aninhadas Um método muito comum no Windows 32 bits é o uso de estruturas aninhadas. O MASM possui uma notação que lida com este tipo de construção. Se você precisar de uma estrutura que possui múltiplas estruturas no seu interior, a coisa funciona assim:
MinhaEstruAninhada STRUCT item1 RECT <> item2 POINT <> MinhaEstruAninhada ENDS
Esta estrutura usa a estrutura RECT (mostrada anteriormente) e a seguinte estrutura POINT: POINT STRUCT x DWORD ? y DWORD ? POINT ENDS
Neste caso, existem seis membros na estrutura "MinhaEstruAninhada", quatro da estrutura RECT e dois da estrutura POINT. Alocada na pilha, tem o seguinte aspecto: LOCAL mea:MinhaEstruAninhada
Os seis membros desta estrutura são: mea.item1.left mea.item1.top mea.item1.right mea.item1.bottom mea.item2.x mea.item2.y
A notação mea.item2.x significa a estrutura alocada mea, seu segundo item (item2) e o primeiro item da estrutura POINT (x). As estruturas podem ser aninhadas em diversas profundidades, mas todas usam esta mesma notação e a mesma lógica.
Uso avançado de estruturas Cada vez mais existe a necessidade de manusear estruturas que são passadas como um endereço e este tipo de codificação está-se tornando comum no design de código Windows. O MASM tem uma notação especializada para facilitar o manuseio. Se, por exemplo, você precisasse passar o endereço de uma estrutura RECT para um procedimento, normalmente iria fazer a chamada da seguinte maneira: invoke MinhaFuncao, ADDR Rct
No final do procedimento onde foi chamada esta função, normalmente haveria algo parecido com o seguinte: MinhaFuncao proc lpRect:DWORD
Com uma estrutura simples como a RECT, você pode endereçar manualmente cada um
dos parâmetros colocando o endereço num registrador e escrevendo na localização de cada membro: mov mov mov mov mov
eax, lpRct
[eax], DWORD PTR 10 [eax+4], DWORD PTR 12 [eax+8], DWORD PTR 14 [eax+12], DWORD PTR 16
Isto funciona muito bem, porém, com estruturas mais complexas, fica mais difícil trabalhar e mais fácil errar. A alternativa é usar um método que o MASM possui para endereçar cada um dos membros: a diretiva ASSUME. ASSUME eax:PTR RECT mov eax, lpRct mov mov mov mov
[eax].left, 10 [eax].top, 12 [eax].right, 14 [eax].bottom, 16
ASSUME eax:nothing
Esta diretiva informa o assembler que o registrador EAX deve ser tratado como uma estrutura RECT. O ASSUME eax:nothing informa o assembler que é para parar de tratar o registrador como uma estrutura RECT. Há uma notação alternativa onde você pode fazer um "type cast" para cada membro: mov eax, lpRct mov mov mov mov
(RECT (RECT (RECT (RECT
PTR PTR PTR PTR
[eax]).left, 10 [eax]).top, 12 [eax]).right, 14 [eax]).bottom, 16
A vantagem desta técnica é que ela usa a conveniência e a confiabilidade de uma estrutura, de modo que você não precisa calcular o offset de cada membro, além de usar os nomes normais dos membros. A desvantagem desta técnica é que ela usa um registrador, o que nem sempre é conveniente. Se o uso do registrador for um problema, você precisará alocar variáveis LOCAIS e copiar os dados de cada membro requerido para estas variáveis.
Finalmentes O assunto parece ser coisa de outro planeta? Se for este o seu caso, ignore este tutorial por enquanto. Em outros tutoriais sobre Assembly estas técnicas serão utilizadas e haverá links para este tutorial. Aí a coisa fica um pouco mais clara, mas se você quiser matar a curiosidade agora mesmo, veja uma aplicação prática no tutorial "Pintando texto".
Abraços da vó Vicki
Neste tutorial vamos lidar com texto. Você deve estar pensando, "Escrever um texto? Grande coisa, e daí?". É que no Windows não se "escreve", se "pinta" o texto. Vamos usar a área cliente de uma janela e um contexto modelo. Leia o tutorial e fique por dentro...
Texto como objeto GUI Se você ainda se lembra, GUI significa Interface Gráfica do Usuário. A novidade é que o Windows trata texto como imagem, portanto, texto é um objeto gráfico ou objeto GUI. Cada caractere é um conjunto de pontos (pixels) dispostos de maneira que adquiram uma aparência peculiar. É por isso que "pintamos" o texto ao invés de escrevê-lo. Observe abaixo o caractere "a" dentro do círculo vermelho: não se vê pontinho nenhum. Logo acima está o mesmo "a" aumentado algumas vezes: os pixels começam a se delinear. Agora observe a ampliação maior: numa matriz de 10 x 10 pontos, alguns estão em azul formando a letra "a". Esta matriz servirá para quaisquer caracteres gráficos que quisermos montar para esta fonte. Aliás, a fonte usada no exemplo é a Courier New.
Podemos imaginar cada caractere da fonte como uma matriz de 10 x 10 pixels, com alguns deles "pintados" e outros "vazios". Para "pintar" uma frase, basta criar uma sequência de matrizes, usando uma para cada caractere, com os respectivos pixels "cheios e vazios".
A área cliente de uma janela A tela do seu monitor pode apresentar vários programas simultaneamente. É claro que precisam existir regras para que um programa não "pinte" coisas na tela do outro. O Windows garante que um programa não invada a janela de outro limitando a área de pintura de cada janela à sua própria área (chamada de área cliente). A área cliente não tem o tamanho da janela. As bordas, por exemplo, não estão incluídas. Sabemos que a área cliente de uma janela não é constante: basta o usuário mudar suas dimensões que ela se modifica. É por isso que é preciso determinar a área cliente dinamicamente. O sistema não permite a ação de "pixadores" - todas as pinturas são rigorosamente controladas. Primeiro é preciso obter uma autorização do Windows para pintar. Depois, o Windows determina o tamanho da área cliente, a fonte, as cores e outros atributos e devolve um manipulador do modelo autorizado. Este modelo é chamado de contexto de dispositivo.
O contexto de dispositivo O contexto de dispositivo é um estrutura de dados (veja mais em "Trabalhando com Estruturas") mantida internamente pelo sistema. Este contexto geralmente está associado a um dispositivo específico, por exemplo, uma impressora ou uma tela de monitor. No caso do monitor, geralmente também está associado a uma janela em particular. Alguns dos valores do contexto de dispositivo são atributos gráficos, como cores e fontes. Quando solicitado, o sistema cria um contexto com valores default. Estes valores podem ser mudados de acordo com as necessidades do programa e é para isso que o Windows devolve um manipulador. Existem três formas de solicitar um manipulador de contexto de dispositivo:
call BeginPaint - como resposta de uma mensagem WM_PAINT, call GetDC - como resposta de outras mensagens e call CreateDC - para criar um contexto de dispositivo próprio.
Observação: após utilizar o contexto de dispositivo é preciso liberá-lo. SEMPRE libere o contexto na MESMA resposta de mensagem que você utilizou para obtê-lo.
A mensagem WM_PAINT O Windows envia uma mensagem WM_PAINT para uma janela para notificá-la de que é preciso refazer a pintura da sua área cliente. Quando uma janela que estava coberta (ou semi-coberta) por outra é novamente mostrada integralmente, o Windows põe uma mensagem WM_PAINT na lista de mensagens da janela em questão. A janela, ao receber esta mensagem, refaz a pintura da sua área cliente. Fica claro que, quando nós quisermos pintar algo na área cliente de uma janela, precisamos interceptar a mensagem WM_PAINT para efetuar o trabalho de pintura.
O retângulo inválido O Windows sempre define a menor área para uma janela que esteja precisando de uma nova pintura. É a menor área retangular que precisa ser atualizada, o chamado retângulo inválido. Refazendo a pintura apenas no retângulo inválido, o sistema deixa de fazer muito trabalho inútil. Quando o Windows detecta um retângulo inválido na área cliente de uma janela, ele envia uma mensagem WM_PAINT para esta janela. Como resposta a esta mensagem, a janela pode obter uma estrutura paintstruct, a qual contém, entre outras informações, as coordenadas do retângulo inválido. Se formos processar uma mensagem WM_PAINT, no mínimo precisamos chamar o procedimento padrão do Windows (com DefWindowProc) ou validar o retângulo inválido com ValidateRect, caso contrário o Windows ficará enviando continuamente mensagens WM_PAINT.
A resposta à mensagem WM_PAINT A seguir encontram-se as etapas de uma resposta à mensagem WM_PAINT: 1. Obter um manipulador de contexto de dispositivo através da função BeginPaint 2. Pintar a área cliente
3. Liberar o manipulador com a função EndPaint Não é preciso validar explicitamente o retângulo inválido - a chamada a BeginPaint faz isso por nós. Entre o par BeginPaint / EndPaint podemos fazer chamadas a funções para tarefas de pintura.
Uma janela com a frase Assembly Numaboa O modelo do código fonte é nosso velho conhecido, o mesmo mostrado no tutorial "". Neste tutorial falaremos apenas do código adicional e, é claro, ele será adicionado na função gerenteMensagem. Veja a função novamente:
gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg == WM_CREATE .ELSEIF uMsg == WM_SIZE .ELSEIF uMsg == WM_PAINT .ELSEIF uMsg == WM_COMMAND .ELSEIF uMsg == WM_CLOSE .ELSEIF uMsg==WM_DESTROY invoke PostQuitMessage, NULL xor eax,eax ret .ENDIF invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret gerenteMensagem endp
Instanciando as variáveis locais Já vimos que vamos precisar de um manipulador do contexto de dispositivo, de uma estrutura PAINTSTRUCT e de uma estrutura RECT. Se você leu o texto de apoio Trabalhando com Estruturas, sabe do que estou falando; se não, faça-o agora, pois vai precisar destas informações. O contexto de dispositivo é gerenciado pelo sistema e precisamos apenas do seu manipulador: vamos chamá-lo de mDC. A estrutura PAINTSTRUCT contém 5 membros: 3 reservados apenas para uso interno do Windows, o que contém o manipulador do contexto de dispositivo e o que contém a informação se o fundo deve ser repintado ou não. Não precisamos nos preocupar com esta estrutura porque, neste caso, apenas o Windows fará uso da mesma. Precisamos apenas instanciá-la que depois o Windows se encarrega de inicializar seus valores. Vamos chamá-la de ps.
A estrutura RECT receberá o nome de eRet. Este tipo de estrutura define as coordenadas do canto superior esquerdo e do inferior direito de um retângulo: typedef struct _RECT { // rc LONG left; // esquerda LONG top; // topo LONG right; // direita LONG bottom; // base } RECT;
Vamos mudar um pouco a forma do procedimento da função gerenteMensagem apenas para não ficar na mesmice: gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL mCM:HDC LOCAL ps:PAINTSTRUCT LOCAL eRet:RECT .IF uMsg == WM_DESTROY invoke PostQuitMessage, NULL xor eax,eax ret .ELSEIF uMsg == WM_PAINT .ELSE invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret .ENDIF xor eax,eax ret gerenteMensagem endp
Iniciando o processo de pintura Quando o programa é iniciado, uma das primeiras coisas que faz é "produzir" a janela principal. Esta janela será pintada na tela, ou seja, receberá uma mensagem WM_PAINT. A partir daí, sempre que houver a necessidade de repintá-la, lhe será enviada a mesma mensagem. É aí que pegamos o gancho e solicitamos uma licença ao sistema para pintarmos nosso texto. Neste exemplo, a licença será solicitada através da chamada à função BeginPaint. Esta função, da user32.dll, prepara a janela especificada para pintura e inicializa a estrutura PAINTSTRUCT enviada com as informações necessárias: HDC BeginPaint( HWND hwnd, // manipulador da janela LPPAINTSTRUCT lpPaint // ponteiro para a estrutura PAINTSTRUCT );
Se tudo correr bem, o sistema nos devolve o manipulador do contexto de dispositivo. Passamos então o manipulador para a nossa variável local mDC: ... .ELSEIF uMsg == WM_PAINT
invoke BeginPaint, hWnd, ADDR ps mov mDC, eax ...
Obtendo a área cliente da janela Com a função GetClientRect obtemos as coordenadas da área cliente de uma janela. Ests coordenadas especificam os cantos superior esquerdo e inferior direito da área cliente. Como as coordenadas cliente são relativas ao canto superior esquerdo de uma área cliente, as coordenadas do canto superior esquerdo são (0,0). BOOL GetClientRect( HWND hWnd, // manipulador da janela LPRECT lpRect // endereço da estrutura para as coordenadas cliente ); ... .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR ps mov mDC, eax invoke GetClientRect, hWnd, ADDR eRet ...
Desenhando o texto Finalmente chegamos no texto... qual era mesmo? Nós sabemos que queremos "Assembly NumaBoa", mas nosso programa ainda não sabe. Precisamos criar uma variável que contenha a string e, aproveitando o embalo, vamos chamar a classe de "Janela" e personalizar o nome da nova janela: ... .DATA NomeClasse db "Janela",0 TituloJanela db "Janela Pintada",0 NossoTexto db "Assembly NumaBoa",0
Agora é só chamar a função DrawText, da user32.dll. DrawText é uma função API de alto nível para saída de texto (a prima pobre desta função é a TextOut). Esta função desenha um texto formatado dentro do retângulo especificado. Ela formata o texto de acordo com o método especificado (alinhando o texto, com quebra de linha, etc): int DrawText( HDC hDC, // manipulador do contexto modelo LPCTSTR lpString, // ponteiro da string do texto int nCount, // comprimento da string, em caracteres LPRECT lpRect, // ponteiro para a estrutura com as dimensões de formatação UINT uFormat // flags de formatação do texto );
O parâmetro nCount especifica o número de caracteres da string. Se nCount for -1, então o parâmetro lpString é considerado como um ponteiro para uma string terminada em zero e DrawText calcula o número de caracteres automaticamente. O uFormat precisa de algumas explicações. uFormat pode ser a combinação de muitos valores diferentes, dos quais os mais comumente usados são:
Valor>
Explicação Alinha o texto na base do retângulo. Este valor precisa ser DT_BOTTOM combinado com DT_SINGLELINE. Determina a largura e a altura do retângulo. Se houver várias linhas de texto, DrawText usa a largura do retângulo apontado pelo parâmetro lpRect e amplia a altura do retângulo de modo que possa conter a última linha do texto. Se houver apenas uma linha DT_CALCRECT de texto, DrawText modifica o lado direito do retângulo de modo que possa conter o último caractere da linha. Em ambos os casos, DrawText retorna a altura do texto formatado mas NÃO desenha o texto. DT_CENTER Centra o texto horizontalmente no retângulo. Expande os caracteres tab. O número default de caracteres por tab DT_EXPANDTABS é oito. DT_LEFT Alinha o texto à esquerda. Desenha sem cortes (clipping). DrawText é um pouco mais rápida DT_NOCLIP quando DT_NOCLIP é usado. DT_RIGHT Alinha o texto à direita. Apresenta o texto numa linha única. Os retornos de carro e quebra DT_SINGLELINE de linha não quebram a linha. DT_TOP Alinha o texto no topo (apenas para linha única). DT_VCENTER Centra o texto verticalmente (apenas para linha única). Queremos nosso texto como linha única e centrado na horizontal e na vertical, portanto: ... .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR ps mov mDC, eax invoke GetClientRect, hWnd, ADDR eRet invoke DrawText, mDC, ADDR NossoTexto, -1, ADDR eRet, DT_SINGLELINE or DT_CENTER or DT_VCENTER ...
Desligando o processo de pintura com EndPaint Lembre-se de que é preciso liberar o manipulador do contexto de dispositivo dentro da mesma resposta em que o criamos. Para não esquecer, eu costumo escrever o invoke BeginPaint e logo depois o invoke EndPaint. O miolo eu preencho posteriormente. Mas vamos lá. Já que a pintura está terminada, vamos liberar o manipulador do contexto de dispositivo mDC chamando EndPaint: BOOL EndPaint(
HWND hWnd, // manipulador da janela CONST PAINTSTRUCT *lpPaint // ponteiro para a estrutura com os dados de pintura ); ... .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR ps mov mDC, eax invoke GetClientRect, hWnd, ADDR eRet invoke DrawText, mDC, ADDR NossoTexto, -1, ADDR eRet, DT_SINGLELINE or DT_CENTER or DT_VCENTER invoke EndPaint, hWnd, ADDR ps ...
Finalmentes
O resultado
O resultado deste novo programa pode ser visto ao lado: a mesma janelinha que já conhecemos, mas agora com um texto colocado bem no centro da sua área cliente. Resumo desta história:
Chame o par BeginPaint / EndPaint como resposta de uma mensagem WM_PAINT. Faça o que quiser com a área cliente entre as duas chamadas. Se quiser repintar a área cliente em resposta a outras mensagens, existem duas possibilidades: o Use o par GetDC / ReleaseDC e faça sua pintura entre estas duas chamadas. o Chame InvalidateRect ou UpdateWindow para invalidar a área cliente inteira, forçando o Windows a colocar uma mensagem na lista de mensagens da sua janela, e faça a sua pintura na seção WM_PAINT.
É isso aí. E como não podia deixar de ser, o arquivo contendo todo o tutorial está disponível para download.
Índice do Artigo Janela Numaboíssima (masm) Janelas como bitmap Inserindo o bitmap
Mais sobre a região gráfica Pintando a janela Todas as páginas Neste tutorial vamos sair do feijão com arroz da janela retangular do Windows e criar um visual totalmente novo. Para criar um gráfico que servirá de pele (skin) para a nossa janela, vamos finalmente programar "de verdade" em assembly. Os mnemônicos utilizados neste tutorial são os de uso mais frequente - é bom a gente começar a se acostumar com eles. Caso você não saiba quais são, dê uma chegadinha na Oficina de Informática e leia o tutorial Curso Relâmpago de Assembly.
Mais sobre o contexto de dispositivo Uma das principais características da API do Windows é a sua independência de dispositivos - os aplicativos win32 podem desenhar e imprimir numa grande variedade deles. O software que dá apoio a essa independência são duas DLLs: a GDI. DLL (Graphics Device Interface - interface de dispositivos gráficos) e um driver de dispositivo (device driver). O driver é próprio do dispositivo usado pelo aplicativo. Por exemplo, se o aplicativo for desenhar na área cliente da sua janela num monitor VGA, a biblioteca usada será a VGA.DLL; se for desenhar numa impressora, será SuaImpressora.DLL. O aplicativo precisa indicar para a GDI qual driver deve ser carregado e, depois de carregado, precisa preparar o dispositivo para a operação de desenho: escolher a espessura e a cor das linhas, o padrão e a cor do pincel, a fonte, a região de corte, etc. Estas tarefas são efetuadas através de um contexto de dispositivo. Um contexto de dispositivo é uma estrutura que define um conjunto de objetos gráficos e seus respectivos atributos, além dos modos gráficos que afetam a saída. Para as operações de desenho e pintura, os objetos gráficos incluem uma caneta para desenhar linhas, um pincel para pintar e preencher, um bitmap para copiar ou fazer a rolagem de porções da tela, uma paleta para definir o conjunto de cores disponíveis, uma região para cortes e outras operações e um caminho. Diferentemente de outras estruturas win32, um aplicativo nunca pode acessar diretamente um contexto de dispositivo sempre atua indiretamente sobre a estrutura através de chamadas a diversas funções.
Os contextos de dispositivo de memória Neste tutorial o interesse maior é nos contextos de dispositivo de memória. Estes contextos armazenam imagens do tipo bitmap para um dispositivo em particular. O formato de cor para o bitmap criado é compatível com o dispositivo associado, por isto este tipo de contexto de dispositivo também é conhecido como contexto de contexto compatível. O bitmap original num contexto de dispositivo de memória é apenas marcador e sua
dimensão é de 1 x 1 pixel. Para que um aplicativo possa começar a desenhar, primeiro é preciso selecionar um objeto bitmap para o contexto, indicando as dimensões (largura e altura) apropriadas. Só depois disto é que o aplicativo pode começar a usar o contexto para armazenar imagens. É coisa do tipo "abrir o buraco" para depois "plantar" o gráfico. Não confunda objeto bitmap (o buraco) com a imagem gráfica (a planta). Quando um aplicativo cria um contexto de dispositivo, o Windows lhe atribui automaticamente um conjunto de objetos padrão (caneta, pincel, paleta, região). Só não existe como padrão um bitmap e um caminho (para formas geométricas). Um aplicativo também pode criar um novo objeto e incluí-lo no contexto de dispositivo. Os tipos de contexto de dispositivo existentes são: display (pintura em vídeo), printer (pintura em impressora), memory (operações de pintura em bitmaps) e information (dispositivos de dados).
Um pouco sobre bitmaps A tradução literal de Bitmap é mapa de bits, ou seja, a cor de cada pixel é identificada por um conjunto de bits. O conjunto de cores designadas para um determinado gráfico é chamado de paleta de cores. Um bitmap de 16 cores possui uma paleta com 16 cores possíveis, portanto, precisamos de 4 bits para poder numerar cada uma das cores. Veja o exemplo abaixo: Decimal Hexa Binário Cor 0 0 0000 preto 1 1 0001 vermelho escuro 2 2 0010 verde escuro 3 3 0011 amarelo escuro 4 4 0100 azul escuro 5 5 0101 magenta escuro 6 6 0110 turquesa escuro 7 7 0111 cinza escuro 8 8 1000 cinza claro 9 9 1001 vermelho claro 10 A 1010 verde claro 11 B 1011 amarelo claro 12 C 1100 azul claro 13 D 1101 magenta claro 14 E 1110 turquesa claro 15 F 1111 branco Com a paleta definida, podemos montar um bitmap sob a ótica de um programador: basta mapear a sequência de pixels atribuindo-lhes os valores das suas cores. No exemplo abaixo, o gráfico é uma área retangular de 9 x 9 pixels e mostra uma casinha. Note que as linhas são mapeadas de baixo para cima porque se trata de um DIB (device independent bitmap - bitmap independente de dispositivo), um tipo muito comum de bitmap. Na coluna "Mapeado em Hexa" encontram-se os valores da cor de cada um dos pixels de acordo com o padrão da paleta de cores. Poderíamos ter adicionado uma coluna com o mapeamento em binário o que, na verdade, seria a representação "real" do bitmap e é de onde deriva o nome deste tipo de imagem.
Linhas linha 8 linha 7 linha 6 linha 5 linha 4 linha 3 linha 2 linha 1 linha 0
Mapeado em Hexa 000090000 000999000 009999900 099999990 9FFFFFFF9 0FFFFFFF0 0FF1F1FF0 0FF1F1FF0 0FF1F1FF0
As janelas como bitmap As janelas nada mais são do que áreas retangulares numa região da tela. Cada uma possui seu próprio modelo de contexto de dispositivo, que contém os objetos gráficos usados para desenhá-la e pintá-la. Portanto, cada janela possui um bitmap que lhe confere a aparência. Se a área retangular estiver totalmente preenchida por pixels coloridos, é claro que a janela terá o formato retangular. Se a área retangular tiver pixels transparentes, apenas as áreas de pixels visíveis é que irão compor o formato da janela. Sabendo disto, podemos desenhar janelas com "buracos", com contornos arredondados ou outras características visuais partindo de uma área retangular e tornando porções desta área "invisíveis" ou transparentes. Precisamos apenas fornecer o bitmap adequado ao dispositivo de contexto modelo janela. Use o programa gráfico da sua preferência para criar o gráfico que servirá de máscara para a janela. Use a mesma cor em todas as áreas do gráfico que você quer que "desapareçam", pois esta cor é que será transformada em "transparente". É claro que precisa ser uma cor diferente de todas as que você usar para as áreas não transparentes. ANOTE as dimensões do gráfico em pixels e salve-o em formato bitmap (.bmp). O gráfico do exemplo tem 350 x 200 pixels e a cor magenta foi escolhida para ser a transparente: Bitmap numaBoa.bmp
Aparência da janela
Este bitmap, para ser incorporado ao executável, precisa ser adicionado ao arquivo de recursos. No tutorial "Usando Recursos" você encontra uma descrição detalhada de como proceder (não se esqueça de compilar o arquivo de recursos). A referência a este gráfico no arquivo RSRC.RC será a seguinte: 1000 BITMAP "numaBoa.bmp"
Criando nossa classe janela Nossa janela deverá ter as mesmas dimensões que o bitmap que servirá de máscara. Podemos inicializar duas constantes com estes valores ou usar os valores quando formos criar a classe. Vamos usar a primeira opção. O fundo da janela pode (e deve) ser NULL. IMPORTANTE é que a janela seja do tipo popup. .386 ... .DATA NomeClasse db "Numaboissima", 0 TituloJanela db "Janela NumaBoíssima", 0 .CONST BitmapID equ 1000 largBitmap equ 350 altBitmap equ 200 .CODE inicio: ... gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD ... mov ej.hbrBackground, NULL ... invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, largBitmap, altBitmap, NULL, NULL, mInst, NULL ... gerenteJanela endp ...
Interceptando o WM_CREATE No momento em que esta janela for criada, queremos que ela faça uso do nosso bitmap e não do padrão do sistema. Faremos o trabalho de pintura para que ele fique de acordo com o nosso projeto.
Carregando o bitmap A primeira providência é carregar o bitmap desejado. Para isto vamos usar a função LoadBitmap (também descrita em "Usando Recursos") e guardar o manipulador do bitmap na variável global mBitmap. .386
... .DATA? mInstancia DWORD ? ... mBitmap DWORD ? .DATA NomeClasse db "Numaboissima", 0 TituloJanela db "Janela NumaBoíssima", 0 .CONST BitmapID equ 1000 largBitmap equ 350 altBitmap equ 200 .CODE inicio: ... gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD ... invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, largBitmap, altBitmap, NULL, NULL, mInst, NULL ... gerenteJanela endp gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg == WM_CREATE invoke LoadBitmap, mInstancia, BitmapID mov mBitmap, eax ... gerenteMensagem endp ...
Criando um contexto de dispositivo de memória Para podermos manipular nosso gráfico antes de apresentá-lo na tela é preciso criar um contexto de dispositivo do tipo memória. É como se fosse o rascunho de trabalho. A função que cria automaticamente um contexto de dispositivo de memória é a CreateCompatibleDC, que pede como parâmetro o manipulador do dispositivo de saída. Se este parâmetro for NULL, a função cria um contexto de dispositivo de memória compatível com a tela atual do aplicativo. O manipulador obtido será guardado na variável local mCMMem. HDC CreateCompatibleDC( HDC hdc // manipulador para o contexto modelo de memória );
gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL mCMMem:HDC .IF uMsg == WM_CREATE invoke LoadBitmap, mInstancia, BitmapID mov mBitmap, eax invoke CreateCompatibleDC, NULL mov mCMMem, eax ... gerenteMensagem endp ...
Inserindo o bitmap no contexto modelo de memória Agora é necessário inserir o objeto bitmap que contém o gráfico da nossa janela no contexto de dispositivo de memória para que possamos utilizá-lo como modelo de pintura. A função SelectObject faz o trabalho: HGDIOBJ SelectObject( HDC hdc, // manipulador do contexto modelo HGDIOBJ hgdiobj // manipulador do objeto ); gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL mCMMem:HDC .IF uMsg == WM_CREATE invoke LoadBitmap, mInstancia, BitmapID mov mBitmap, eax invoke CreateCompatibleDC, NULL mov mCMMem, eax invoke SelectObject, mCMMem, mBitmap invoke ReleaseDC, hWnd, mCM ... gerenteMensagem endp ...
Obtendo as coordenadas da área da janela As coordenadas de tela do canto superior esquerdo e inferior direito da janela servem de referência para definir a área retangular total da janela. Chamando a função GetWindowRect obtemos os quatro valores que serão armazenados numa estrutura do tipo RECT . BOOL GetWindowRect( HWND hWnd, // manipulador da janela LPRECT lpRect // endereço da estrutura para as coordenadas da janela );
De posse destas coordenadas, vamos chamar a função grafiti. Esta é uma função própria (não é da API do Windows), cheia de emoções gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL mCMMem:HDC LOCAL retang:RECT .IF uMsg == WM_CREATE invoke LoadBitmap, mInstancia, BitmapID mov mBitmap, eax invoke CreateCompatibleDC, NULL mov mCMMem, eax invoke SelectObject, mCMMem, mBitmap invoke GetWindowRect, hWnd, ADDR retang invoke grafiti, mCMMem, retang.right, retang.bottom ... gerenteMensagem endp ...
Criando a Região Gráfica Agora vamos entrar no "filé" do projeto. Todas as condições necessárias para podermos pintar nossa janela estão preparadas. Agora pegue a lata de tinta e dá-lhe pintura! Para efetuar este serviço, vamos criar uma função própria, a grafiti.
Declarando uma função própria A função grafiti precisa de três parâmetros para trabalhar, todos do tipo DWORD: o manipulador do contexto de dispositivo de memória, a altura e a largura da janela. Vamos escrever seu protótipo, seu cabeçalho e seu final: .386 .. gerenteJanela proto :DWORD, :DWORD, :DWORD, :DWORD grafiti proto :DWORD, :DWORD, :DWORD ... grafiti proc USES ESI EDI EBX _mModelo:HDC, _largura:DWORD, _altura:DWORD grafiti endp
A palavra-chave USES Esta declaração do procedimento contém uma novidade: foi incluída a palavra-chave USES. Quando você programa para Win32, é preciso conhecer algumas regras importantes. Uma dessas regras é que o Windows usa ESI, EDI, EBP e EBX internamente e não espera que os valores destes registradores sejam alterados. Portanto, lembre-se da primeira regra: se você usar qualquer um destes quatro registradores numa função callback, não esqueça de restaurar seus valores originais antes de devolver o controle ao
Windows. Uma função callback é uma função sua que é chamada pelo Windows. Isto não significa que você não possa utilizar estes quatro registradores. Apenas certifique-se de que seus valores sejam restaurados antes de devolver o controle ao Windows. O MASM possui uma palavra-chave opcional que pode ser usada com PROC. Esta palavra-chave faz com que o assembler gere código para "pushar" para a pilha o valor dos registradores que devem ser preservados (e que serão alterados no procedimento) e para reavê-los da pilha com pop quando o procedimento retorna. USES aceita uma lista de registradores separados por um espaço.
Identificando a cor transparente Partimos do princípio de que o primeiro pixel (o das coordenadas 0,0) tenha a cor que queremos transparente. O que precisamos é identificar todos os pixels que tenham uma cor diferente da cor que deve ficar transparente e fazer uma cópia dos mesmos. Ficando no exemplo acima, seria como se usássemos nosso bitmap original e fizéssemos apenas a cópia dos pixels diferentes de preto: Pixels Originais
Cópia dos Pixels
Para obter a cópia desejada, vamos analisar todos os pixels do bitmap percorrendo todas as linhas, coluna a coluna. O registrador EDI mostrará o número da coluna e o ESI o número da linha. Para usá-los no nosso procedimento, a primeira providência será zerálos com XOR. O valor da cor transparente, em RGB (red, green, blue), ficará na variável local corT. A função GetPixel, da gdi.lib, obtém o valor RGB (red, green, blue) do pixel especificado pelas coordenadas. COLORREF GetPixel( HDC hdc, // manipulador do contexto modelo int XPos, // coordenada x do pixel int nYPos // coordenada y do pixel ); .386 ... include \masm32\include\user32.inc include \masm32\include\gdi32.inc ... includelib \masm32\lib\user32.lib includelib \masm32\lib\gdi32.lib ...
grafiti proc USES ESI EDI EBX _mModelo:HDC, _largura:DWORD, _altura:DWORD LOCAL corT:DWORD xor edi, edi xor esi, esi invoke GetPixel, _mModelo, 0, 0 mov corT, eax grafiti endp
Verificando cada pixel Com a ajuda de rótulos, vamos fazer primeiro um loop para percorrer todos os pixels do bitmap e comparar a cor do pixel atual com a cor transparente. grafiti proc USES ESI EDI EBX _mModelo:HDC, _largura:DWORD, _altura:DWORD LOCAL corT: DWORD xor edi, edi xor esi, esi invoke GetPixel, _mModelo, 0, 0 mov corT, eax transparência
; ; ; ;
zera o registrador edi zera o registrador esi obtém a cor do primeiro pixel inicializa corT com a cor da
_olhaPix: invoke GetPixel, _mModelo, edi, esi cmp eax, corT transparente _proxPix: inc edi cmp edi, _largura bitmap jbe _olhaPix pixel
; obtém a cor do pixel atual ; compara a cor atual com a
; incrementa a coluna ; compara a coluna com a largura do ; se for menor ou igual, olha o próximo
xor edi, edi inc esi cmp esi, _altura jb _olhaPix
; ; ; ; ;
se for maior... zera o contador de colunas (coluna 0) passa para a próxima linha compara a linha com a altura do bitmap se for menor, olha o próximo pixel
_retorna:
; termina o processo
grafiti endp
Copiando os pixels não transparentes No nosso exemplo, na segunda fileira, quando alcançarmos a coluna 4, encontraremos o primeiro pixel vermelho. Na coluna 6 encontra-se o último pixel vermelho desta série.
Portanto, teremos que copiar os pixels 4, 5 e 6 desta fileira. Para determinar as coordenadas desta região numere as linhas horizontais e verticais a partir do ponto 0,0. A segunda linha horizontal é a linha 1 (x = 1) e a quarta linha vertical é a linha 3 (y = 3). Estas são as coordenadas do canto superior esquerdo da região (1,3). Repetindo o raciocínio para o último pixel vermelho desta fileira, verificamos que seu canto inferior direito é delimitado pela terceira linha horizontal (linha 2) e pela sétima linha vertical (linha 6). Suas coordenadas são x = 2 e y = 3). No esquema abaixo estão as coordenadas de cada região que deverá ser copiada: 0,0
Coordenada Superior Esquerda x=4ey=0 x=3ey=1 x=2ey=2 x=1ey=3 x=0ey=4 x=1ey=5 ...
Coordenada Inferior Direita x=5ey=1 x=6ey=2 x=7ey=3 x=8ey=4 x=9ey=5 x=8ey=6 ...
A lógica do nosso procedimento será a seguinte:
Percorrer cada fileira analisando bit por bit. Quando encontrar o primeiro bit não transparente, marcar as coordenadas do canto superior esquerdo. Continuar até encontrar o próximo bit transparente (acabou a sequência dos coloridos) ou o fim da fileira: o marcar as coordenadas do canto inferior direito o fazer a cópia dos bits coloridos usando as coordenadas obtidas o se for a primeira cópia, esta inicializará o "copião" o se não for a primeira cópia, adicioná-la ao "copião" Retornar o copião
Os pontos vitais desta rotina são: identificar uma sequência de pixels não transparentes e determinar a primeira cópia (que servirá de "copião"). Para isto faremos uso de duas variáveis locais. A variável temCor nos indicará se o pixel analisado é transparente ou não e a variável fazerCopiao indicará se o "copião" já foi inicializado. Convencionaremos que o primeiro pixel seja da cor que queremos transparente - é o pixel indicador de transparência. Sabendo disto, no início do processo a variável temCor deve ser falsa (FALSE) e depois sempre deve refletir o estado do pixel que está sendo analisado. A primeira cópia de pixels não transparentes servirá para inicializar o "copião"; as cópias subsequentes serão apenas adicionadas ao mesmo. Portanto, no início do processo, precisamos indicar que o "copião" precisa ser feito, ou seja, fazCopiao deve ser verdadeiro (TRUE). No esquema abaixo, siga os pontos (•) e acompanhe a lógica:
Fileira 1 (linha 0)
fazCopiao
temCor
esi (linha)
TRUE
FALSE
0
•
•
TRUE -> FALSE
FALSE -> TRUE TRUE -> FALSE
•
FALSE
FALSE
TRUE
•
Fileira 2 (linha 1) •
FALSE
• • • •
0 0
Faz cópia de ebx,esi edi ebx edi,esi+1 3 -> 4 4 -> 4 5 4,0 e 5,1
5 5 -> 6
0
fazCopiao
temCor
esi (linha)
FALSE
FALSE
1
FALSE -> TRUE
1
FALSE
TRUE
1
FALSE
TRUE
1
FALSE
TRUE -> FALSE
1
Faz cópia de ebx,esi edi ebx edi,esi+1 2 -> 3 3 -> 4
3 4 -> 5 5 -> 6
3,1 e 6,2
6
(adiciona ao copião) Para abrigar as cópias das regiões com pixels não transparentes vamos precisar de um contexto de dispositivo que funcionará como um temporário, o CMtemp, e de um contexto de dispositivo para o "copião", o CMcopiao. Agora é possível completar nosso processo: grafiti proc USES ESI EDI EBX _mModelo:HDC, _largura:DWORD, _altura:DWORD LOCAL corT:DWORD LOCAL copiao:DWORD LOCAL temCor:DWORD LOCAL CMcopiao: DWORD LOCAL CMtemp: DWORD xor edi, edi xor esi, esi mov temCor, FALSE mov copiao, TRUE
; ; ; ; ;
zera o registrador edi (coluna 0) zera o registrador esi (linha 0) como o primeiro pixel é transparente, temCor precisa ser falso o copião precisa ser feito
; obtém a cor do primeiro pixel invoke GetPixel, _mModelo, 0, 0 mov corT, eax ; inicializa corT com a cor da transparência
_olhaPix:
; obtém a cor do pixel da linha invoke GetPixel, _mModelo, edi, esi atual na coluna atual ; a cor do pixel atual é transparente? cmp eax, corT ; sim, então verifica em _copiaPix se é o primeiro jz _copiaPix transparente depois de colorido(s) ; não é transparente. Está dentro da largura? cmp edi, _largura jnz _acheiPix ; sim, então atualiza o indicador temCor
_copiaPix: cmp temCor, TRUE jnz _proxPix
; o pixel anterior era colorido? ; não, então vai para o próximo pixel
; mov temCor, FALSE ; mov eax, esi inc eax ; invoke CreateRectRgn, pixels coloridos ; mov CMtemp, eax
cmp copiao, TRUE jnz _poeCopiao push CMtemp pop CMcopiao mov copiao, FALSE jmp _proxPix
; ; ; ; ; ;
sim, mas o atual é transparente pega a coordenada x superior incrementa para obter a coordenada x inferior ebx, esi, edi, eax ; faz a cópia da região de põe a cópia no contexto de dispositivo temporário a cópia temporária é a primeira? não, então adicione ao copião sim, pega a cópia temporária e a transforma em copião desliga o indicador e analisa o próximo pixel
_poeCopiao: ; combina a invoke CombineRgn, CMcopiao, CMcopiao, CMtemp, RGN_OR cópia temporária com o copião ; "limpa" o contexto modelo temporário invoke DeleteObject, CMtemp jmp _proxPix ; e analisa o próximo pixel _acheiPix: cmp temCor, FALSE jnz _proxPix mov temCor, TRUE mov ebx, edi _proxPix: inc edi cmp edi, _largura jbe _olhaPix xor edi, edi inc esi cmp esi, _altura jb _olhaPix _retorna: mov eax, CMcopiao ret grafiti endp
; ; ; ;
pixel é colorido. O indicador já foi ajustado? sim, então analise o próximo pixel não, então ajuste o indicador temCor e guarde a coordenada x em ebx
; ; ; ; ; ; ; ;
incrementa a coluna a coluna é a última da linha? se for menor ou igual, olha o próximo pixel se for maior... é maior, então zera o contador de colunas passa para a próxima linha a linha é a última do bitmap? não, então olha o próximo pixel
; sim, então põe o copião em eax para retornar ; retorna para o ponto de chamada
Regiões Foram usadas algumas funções que ainda não foram vistas. A CreateRectRgn (que cria uma região retagular), a CombineRgn (que funde duas regiões retangulares) e a DeleteObject , todas da GDI32.DLL. Uma região é um retângulo, polígono ou elipse (ou a combinação de duas ou mais destas formas) que pode ser preenchida, pintada, invertida, emoldurada e que pode ser usada para testar a localização do cursor (chamado de hit testing). CreateRectRgn cria uma região retangular, CreateEllipticRgn cria uma região elíptica, e assim por diante. As regiões assim criadas podem ser inseridas num contexto de dispositivo para que o aplicativo possa operar sobre elas. A função CreateRectRgn cria uma região retangular e retorna um manipulador para a região criada: HRGN CreateRectRgn( int nLeftRect, // coordenada x do canto superior esquerdo da região int nTopRect, // coordenada y do canto superior esquerdo da região int nRightRect, // coordenada x do canto inferior direito da região int nBottomRect // coordenada y do canto inferior direito da região );
A função CombineRgn combina duas regiões e armazena o resultado numa terceira região: int CombineRgn( HRGN hrgnDest, // HRGN hrgnSrc1, // HRGN hrgnSrc2, // int fnCombineMode );
manipulador da região destino manipulador da região origem manipulador da região origem // modo de combinação
O modo de combinação define o resultado desejado e pode ser RGN_AND, RGN_COPY, etc. No nosso exemplo foi usado o modo RGN_OR porque queremos "somar" as regiões. Veja abaixo:
Devolvendo o controle ao gerente de mensagens
Após ter percorrido um a um os pixels do nosso bitmap e "montado" uma região que contém apenas os pixels não transparentes, vamos devolver o controle ao gerente de mensagens para que possa fazer uso desta máscara na janela.
Incorporando a região gráfica Retornando da função grafiti, o manipulador para a região com o bitmap trabalhado, onde vazamos todos os pixels da cor que foi determinada para ser transparente, se encontra no registrador EAX. Existe uma função da API para incorporar a região gráfica a uma janela. É a SetWindowRgn: int SetWindowRgn( HWND hWnd, // manipulador da janela que incorporará a região HRGN hRgn, // manipulador da região BOOL bRedraw // indicador de repintura );
Através desta função podemos trocar o bitmap original (aquele que nós carregamos e transferimos para o mBitmap) pelo novo bitmap obtido através da função grafiti. Fazendo a troca, termina o nosso serviço de criar a janela. Certifique-se de ter liberado o contexto de dispositivo para que outros aplicativos possam utilizá-lo. Além disso, o contexto de dispositivo de memória não é mais necessário e DEVE ser eliminado com DeleteDC. gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL mCMMem:HDC LOCAL retang:RECT .IF uMsg == WM_CREATE invoke LoadBitmap, mInstancia, BitmapID mov mBitmap, eax invoke GetWindowDC, hWnd mov mCM, eax invoke CreateCompatibleDC, NULL mov mCMMem, eax invoke SelectObject, mCMMem, mBitmap invoke GetWindowRect, hWnd, ADDR retang invoke grafiti, mCMMem, retang.right, retang.bottom invoke SetWindowRgn, hWnd, eax, TRUE invoke DeleteDC, mCMMem ...
Pintando a janela com a máscara Assim que uma janela é criada, ato contínuo ela começa a ser pintada. Precisamos monitorar a pintura para garantir que nosso bitmap "transparentado", obtido a duras penas, seja utilizado como máscara da nossa janela. A primeira providência é iniciar um sessão de pintura com BeginPaint e declarar uma variável local para receber o
manipulador do contexto de dispositivo obtido e uma do tipo PAINTSTRUCT para receber as informações sobre a pintura. Depois precisamos obter a área retangular da janela com a função GetWindowRect e criar um contexto de dispositivo compatível com CreateCompatibleDC (se tiver dúvidas, veja novamente no tutorial "Pintando Texto"). Em seguida selecionamos o bitmap com SelectObject. Lembre-se de que todo BeginPaint precisa terminar com um EndPaint para liberar o contexto de dispositivo para outras operações. gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL mCM:HDC LOCAL mCMMem:HDC LOCAL retang:RECT LOCAL ps:PAINTSTRUCT .IF uMsg == WM_CREATE invoke LoadBitmap, mInstancia, BitmapID mov mBitmap, eax invoke CreateCompatibleDC, NULL mov mCMMem, eax invoke SelectObject, mCMMem, mBitmap invoke GetWindowRect, hWnd, ADDR retang invoke grafiti, mCMMem, retang.right, retang.bottom invoke SetWindowRgn, hWnd, eax, TRUE invoke DeleteDC, mCMMem .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR ps invoke GetWindowRect, hWnd, ADDR retang mov mCM, eax invoke CreateCompatibleDC, NULL mov mCMMem, eax invoke SelectObject, mCMMem, hBitmap invoke EndPaint, hWnd, ADDR ps ...
Transferindo blocos de bits Agora é só transferir as "cores dos pixels" para a área retangular da nossa janela e tchan, tchan, tchan, tchan... vestimos nossa janela com a máscara desejada. A função responsável pela transferência dos blocos de bits referentes às cores dos pixels é a BitBlt (BIT-BLock Transfer) que exige um caminhão de parâmetros: BOOL BitBlt( HDC hdcDest, // manipulador do contexto modelo destino int nXDest, // coordenada x do canto superior esquerdo do retângulo destino int nYDest, // coordenada y do canto superior esquerdo do retângulo destino int nWidth, // largura do retângulo destino
int nHeight, // altura do retângulo destino HDC hdcSrc, // manipulador do contexto modelo fonte int nXSrc, // coordenada x do canto superior esquerdo do retângulo fonte int nYSrc, // coordenada y do canto superior esquerdo do retângulo fonte DWORD dwRop // código da operação para combinação de cores ); .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR retang invoke GetWindowRect, hWnd, ADDR ps mov mCM, eax invoke CreateCompatibleDC, NULL mov mCMMem, eax invoke SelectObject, mCMMem, hBitmap invoke BitBlt, mCM, 0, 0, retang.right, retang.bottom, mCMMem, 0, 0, SRCCOPY invoke DeleteDC, mCMMem invoke EndPaint, hWnd, ADDR ps ...
Como último parâmetro da função BitBlt usamos o código SRCCOPY (source copy cópia da fonte), indicando que queremos uma simples cópia. Eliminamos mCMMem porque sempre é bom livrar um pouco de memória e... NOSSA JANELA ESTÁ PRONTA!!! Compile o arquivo de recursos, compile e linke o código fonte. Divirta-se com o executável. É aí que aparecem alguns "senões": a janela não oferece nenhuma possibilidade visível de ser fechada e a janela não pode ser arrastada. Por enquanto, nos testes, é preciso usar Alt+F4 para fechá-la.
Permitindo o arraste da janela O arraste da janela é efetuado mantendo o botão esquerdo do mouse pressionado enquanto se arrasta a janela. Pressionar um botão do mouse começa sempre com "abaixar", ou seja, BUTTON DOWN. Cada vez que o botão esquerdo do mouse é "abaixado", a janela recebe uma mensagem WM_LBUTTONDOWN (o L vem de LEFT, esquerdo). Vamos caçar esta mensagem e reenviá-la com os nossos parâmetros: LRESULT SendMessage( HWND hWnd, // manipulador da janela destino UINT Msg, // mensagem a ser enviada WPARAM wParam, primeiro parâmetro da mensagem LPARAM lParam // segundo parâmetro da mensagem );
A mensagem WM_NCLBUTTONDOWN é enviada quando o usuário pressiona o botão esquerdo do mouse com o cursor FORA da área cliente da janela. Como estamos trabalhando com a área total da janela, o cursor está sempre fora da área cliente, portanto... vamos capturar a mensagem de "botão abaixado" e corrigí-la para "fora da área cliente". Para isto, precisamos conhecer melhor a mensagem WM_NCLBUTTONDOWN:
WM_NCLBUTTONDOWN nHittest = (INT) wParam; // valor do hit-test pts = MAKEPOINTS(lParam); // posição do cursor );
O hit-test (teste do ponto clicado) é um valor retornado pela DefWindowProc (o gerente de mensagens padrão do Windows) como resultado do processamento de uma mensagem WM_NCHITTEST. Esta mensagem é enviada para uma janela quando o cursor se movimenta ou quando um botão do mouse é pressionado ou solto. Nós não interceptamos esta mensagem, portanto ela entrou no processamento padrão do DefWindowProc. Dos diversos valores de retorno do processamento de uma mensagem WM_NCHITTEST, aquele que nos interessa é o HTCAPTION. Este valor indica que o clique ocorreu na barra de título da janela, ou seja, a única área da janela que permite o arrasto. Vamos "enganar" o sistema e informar que todo e qualquer clique ocorre na barra de título A posição do cursor não é importante, pois não dependemos dela. Podemos simplesmente informar o valor 0 (coordenadas 0,0 da janela). .ELSEIF uMsg == WM_LBUTTONDOWN invoke SendMessage, hWnd, WM_NCLBUTTONDOWN, HTCAPTION, 0 ...
Fechando a janela com um duplo clique Já que estamos interceptando mensagens do mouse, vamos associar um duplo-clique do botão esquerdo do mouse com o fechamento da janela. Mas, para que a janela aceite cliques duplos, é preciso dar-lhe esta característica ao definirmos sua classe: gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD ... mov ej.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS ... invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, largBitmap, altBitmap, NULL, NULL, mInst, NULL ... gerenteJanela endp
Com a janela preparada, podemos interceptar a mensagem WM_LBUTTONDBLCLK (Left BUTTON DouBLe CLicK - duplo clique do botão esquerdo) e trocá-la pela mensagem WM_DESTROY, que não precisa de parâmetros específicos e fecha o aplicativo: .ELSEIF uMsg == WM_LBUTTONDBLCLK invoke SendMessage, hWnd, WM_DESTROY, NULL, NULL ...
Serviço de faxina Quando escrevemos um código fonte, sempre é aconselhável fazer uma revisão final para verificar se existem variáveis definidas e não inicializadas/usadas, se criamos algum objeto que não é eliminado automaticamente pelo sistema após o seu uso ou se iniciamos um processo que necessita de fechamento. É o caso do BeginPaint que precisa do EndPaint correspondente e do contexto de dispositivo que precisa ser liberado para o uso geral. Destes nós já cuidamos. Um objeto Bitmap também precisa ser eliminado, portanto, ponha na sua lista de faxina: após um LoadBitmap SEMPRE precisa existir um DeleteObject para o(s) bitmap(s) carregado(s). Se não fizermos esta "limpeza" antes de encerrarmos o aplicativo, vai sobrar lixo e, geralmente, dos grandes. Nós usamos a função LoadBitmap quando interceptamos a mensagem WM_CREATE e o manipulador do bitmap foi armazenado em mBitmap. Enquanto o aplicativo estiver rodando, precisamos deste bitmap para fazer a pintura da máscara da janela (interceptando WM_PAINT), portanto, não podemos eliminá-lo durante a execução. Só nos resta a alternativa de usar a DeleteObject na saída do programa, ou seja, quando interceptamos a mensagem WM_DESTROY: .ELSEIF uMsg == WM_DESTROY invoke DeleteObject, mBitmap invoke PostQuitMessage, NULL ...
É isso aí, moçada. A novela chegou ao fim. Compilem o executável e bom divertimento
Download Tutorial para download Este tutorial, juntamente com o código fonte, imagens e o executável está na seção de Downloads / Tutoriais / Assembly Numaboa, mas você também pode baixá-lo aqui.
Fontes Este tutorial se baseia no exemplo "CUSTOM WINDOWS SHAPE" escrito por mob (também conhecido como drcmda), incluído no pacote MASM32 do hutch.
Já que no tutorial anterior (o Janela Numaboíssima) foi visto como fazer
uma janela diferente, vamos aproveitar o código fonte e fazer com que a nova janela troque de pele. Para compensar o tutorial anterior, extremamente extenso, neste você vai ter apenas a listagem do código fonte do tutorial anterior com as pequenas modificações explicadas.
Explicações preliminares O projeto é para uma janela de formato n~qo convencional que, com um clique no bot~qo direito do mouse, mude de pele. Precisamos no mínimo de dois bitmaps, de dimensões iguais, que possam servir de máscara. Um deles será o do tutorial anterior; o outro tem o Kiki como garoto propaganda. O arquivo de recursos precisa conter os dois bitmaps. A janela continua sendo fechada com um duplo clique do botão esquerdo do mouse.
Pele "Numaboa"
O código fonte .386 .model flat,stdcall option casemap:none include include include include
\masm32\include\windows.inc \masm32\include\user32.inc \masm32\include\kernel32.inc \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\gdi32.lib gerenteJanela proto :DWORD,:DWORD,:DWORD,:DWORD grafiti proto :DWORD, :DWORD, :DWORD .DATA? mInstancia dd ? mBitmap1 dd ? // manipulador para o bitmap "NumaBoa" mBitmap2 dd ? // manipulador para o bitmap "Kiki" .DATA NomeClasse db "Peles",0 TituloJanela db "Peles NumaBoa",0 Texto db "tchauzinho...",0 bitAtual db 1 // pele ativa: (1) "NumaBoa" e (2) "Kiki"
.CONST Bitmap1ID equ 1000 // identificador do bitmap "NumaBoa" nos recursos Bitmap2ID equ 1001 // identificador do bitmap "Kiki" nos recursos largBitmap equ 350 altBitmap equ 200 .CODE inicio: invoke GetModuleHandle, NULL mov mInstancia, eax invoke gerenteJanela, mInstancia, NULL, NULL, SW_SHOWDEFAULT invoke ExitProcess, eax gerenteJanela PROC mInst:DWORD mInst:DWORD, , mInstAnt:DWORD mInstAnt:DWORD, , linhaCmd:DWORD linhaCmd:DWORD, , Mostra:DWORD Mostra:DWORD LOCAL ej:WNDCLASSEX LOCAL mJanela:HWND LOCAL malote:MSG mov ej.cbSize, SIZEOF WNDCLASSEX mov ej.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS mov ej.lpfnWndProc, OFFSET gerenteMensagem mov ej.cbClsExtra, NULL mov ej.cbWndExtra, NULL push mInstancia pop ej.hInstance mov ej.hbrBackground, NULL mov ej.lpszMenuName, NULL mov ej.lpszClassName, OFFSET NomeClasse invoke LoadIcon, NULL, IDI_WINLOGO mov ej.hIcon, eax mov ej.hIconSm, eax invoke LoadCursor, NULL, IDC_ARROW mov ej.hCursor, eax invoke RegisterClassEx, ADDR ej invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR NomeClasse, WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, largBitmap, altBitmap, NULL, NULL, mInst, NULL mov mJanela,eax invoke ShowWindow, mJanela, SW_SHOWNORMAL invoke UpdateWindow, mJanela _Ini: invoke GetMessage, ADDR malote, NULL, 0, 0 test eax, eax jz _Fim invoke TranslateMessage, ADDR malote invoke DispatchMessage, ADDR malote jmp _Ini _Fim:
mov eax,malote.wParam ret gerenteJanela ENDP gerenteMensagem PROC hWnd:DWORD hWnd:DWORD, , uMsg:DWORD uMsg:DWORD, , wParam:DWORD wParam:DWORD, , lParam:DWORD lParam:DWORD LOCAL mCM:HDC LOCAL mCMBit:HDC // Só o nome foi mudado LOCAL retang:RECT LOCAL ps:PAINTSTRUCT .IF uMsg == WM_CREATE invoke LoadBitmap, mInstancia, Bitmap2ID // carrega o bitmap "Kiki" mov mBitmap2, eax // manipulador para o bitmap "Kiki" invoke LoadBitmap, mInstancia, Bitmap1ID // carrega o bitmap "NumaBoa" mov mBitmap1, eax invoke CreateCompatibleDC, NULL mov mCMBit, eax invoke SelectObject, mCMBit, mBitmap1 invoke GetWindowRect, hWnd, ADDR retang invoke grafiti, mCMBit, retang.right, retang.bottom invoke SetWindowRgn, hWnd, eax, TRUE invoke DeleteDC, mCMBit .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR ps mov mCM, eax invoke GetClientRect, hWnd, ADDR retang // USE ClientRect e NÃO WindowRect invoke CreateCompatibleDC, NULL mov mCMBit, eax .IF bitAtual == 1 invoke SelectObject, mCMBit, mBitmap1 // seleciona a pele "NumaBoa" .ELSE invoke SelectObject, mCMBit, mBitmap2 // seleciona a pele "Kiki" .ENDIF invoke grafiti, mCMBit, retang.right, retang.bottom // prepara o bitmap invoke SetWindowRgn, hWnd, eax, TRUE // transfere para a janela invoke BitBlt, mCM, 0, 0, retang.right, retang.bottom, mCMBit, 0, 0, SRCCOPY invoke DeleteDC, mCMBit invoke EndPaint, hWnd, ADDR ps .ELSEIF uMsg == WM_DESTROY invoke DeleteObject, mBitmap1 // livra a memória do bitmap "NumaBoa" invoke DeleteObject, mBitmap2 // e também do bitmap "Kiki" invoke PostQuitMessage, NULL
xor eax, eax ret .ELSEIF uMsg == WM_LBUTTONDBLCLK invoke MessageBox, hWnd, ADDR Texto, ADDR TituloJanela, MB_OK invoke SendMessage, hWnd, WM_DESTROY, NULL, NULL .ELSEIF uMsg == WM_LBUTTONDOWN invoke SendMessage, hWnd, WM_NCLBUTTONDOWN, HTCAPTION, 0 .ELSEIF uMsg == WM_RBUTTONDOWN // intercepta o clique do botão direito do mouse .IF bitAtual == 1 // se a pele for "NumaBoa" mov bitAtual, 2 // troca para "Kiki" .ELSE mov bitAtual, 1 // e vice-versa .ENDIF invoke GetClientRect, hWnd, ADDR retang // obtém as coordenadas da janela invoke InvalidateRect, hWnd, ADDR retang, TRUE // invalida para poder fazer nova pintura invoke UpdateWindow, hWnd // for força ça a pintura da janela .ENDIF invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret gerenteMensagem ENDP grafiti PROC USES ESI EDI EBX _mModelo:HDC, _largura:DWORD _largura:DWORD, , _altura:DWORD _altura:DWORD LOCAL corT: DWORD LOCAL copiao: DWORD LOCAL temCor: DWORD LOCAL CMtemp: DWORD LOCAL CMcopiao: DWORD mov mov xor xor
temCor, FALSE copiao, TRUE edi, edi esi, esi
invoke GetPixel, _mModelo, 0, 0 mov corT, eax _olhaPix: invoke GetPixel, _mModelo, edi, esi cmp eax, corT jz _copiaPix cmp edi, _largura jnz _acheiPix _copiaPix: cmp temCor, TRUE
jnz _proxPix mov temCor, FALSE mov eax, esi inc eax invoke CreateRectRgn, ebx, esi, edi, eax mov CMtemp, eax cmp copiao, TRUE jnz _poeCopiao push CMtemp pop CMcopiao mov copiao, FALSE jmp _proxPix _poeCopiao: invoke CombineRgn, CMcopiao, CMcopiao, CMtemp, RGN_OR invoke DeleteObject, CMtemp jmp _proxPix _acheiPix: cmp temCor, FALSE jnz _proxPix mov temCor, TRUE mov ebx,edi _proxPix: inc edi cmp edi, _largura jbe _olhaPix xor edi, edi inc esi cmp esi, _altura jb _olhaPix _retorna: mov eax, CMcopiao ret grafiti ENDP end inicio
Um pouco mais de recursos. Como escrever um recurso de menu, como incorporá-lo à sua janela e como gerenciar seus itens. Fácil, fácil. Neste tutorial vamos explorar um pouco mais os recursos, focalizando especificamente os menus.
Sobre os menus O menu é um dos componentes mais importantes da sua janela, pois apresenta uma lista
de serviços que seu programa oferece aos usuários. Por mais que se queira inovar, os usuários já se acostumaram com um "padrão" de menu universalmente aceito: precisa ter como primeiro item [Arquivo] - [File], [ File], com os recursos de abrir, salvar, salvar como, imprimir, sair, etc, e como último item [Ajuda] - [Help]. Geralmente o segundo item é [Editar] - [Edit], com as opções de copiar, colar, procurar, etc. Quanto mais simples, mais eficiente será o menu. Procure indicar com reticências (...) se o item chamar uma caixa de diálogo e não se esqueça de adicionar as teclas de atalho que correspondem a cada item (se houver). Menus são um tipo de recurso. r ecurso. Já vimos no tutorial Usando Recursos que existem vários tipos de recursos como ícones, bitmaps, diálogos, menus, etc. Os recursos são descritos num arquivo em separado, o chamado arquivo de recursos, geralmente com a extensão .rc. Também já vimos que existe uma linguagem própria para descrever os recursos, a "Resource Script Language". Podemos usar qualquer editor de texto para escrevê-los. Os arquivos de recursos precisam ser compilados e depois combinados com o código fonte do seu programa na fase de linkedição. Um recurso menu define a aparência e a funcionalidade de um menu.
Sintaxe do recurso menu Inicialmente defina o nome, o tipo e a área que conterá as características do recurso: meuMenu MENU { ... lista do menu }
Neste caso, o nome do recurso é meuMenu, o tipo é MENU e a área está delimitada por chaves. As chaves podem ser substituídas por BEGIN e END. Na lista do menu podemos usar as palavras-chave MENUITEM e POPUP. A declaração POPUP é para itens especiais que mostram uma lista de sub-itens quando são selecionados. selecionados. A declaração MENUITEM define um item único, sem sub-itens. A sintaxe de MENUITEM é a seguinte: MENUITEM "&texto", ID, [lista de opções] MENUITEM SEPARATOR
&texto: o texto do item (ou seu nome), uma string entre aspas duplas. O sinal & precede a letra da tecla aceleradora. ID: especifica o resultado quando o item é selecionado, ou seja, é o IDentificador IDentificador do item. O ID é sempre um número inteiro. [lista de opções]: este parâmetro opcional especifica a aparência do item. Podemos usar uma ou mais das seguintes opções, separadas por vírgula ou espaço: o CHECKED - o item possui um (tique) marcador o GRAYED - item inativo em cor cinza o HELP - identifica um item de ajuda
o o o
INACTIVE - o item é mostrado mas não pode ser selecionado MENUBARBRAKE - o mesmo que MENUBRAKE MENUBREAK - posiciona o item numa nova linha
A forma MENUITEM SEPARATOR da declaração MENUITEM cria um item de menu inativo que serve como barra divisória entre dois itens de menu ativos. A sintaxe de POPUP é: POPUP "&texto", [lista de opções] BEGIN ... definições dos itens END
[lista de opções] o CHECKED - o item possui um (tique) marcador o GRAYED - item inativo em cor cinza o INACTIVE - o item é mostrado mas não pode ser selecionado o MENUBARBRAKE - separa colunas com uma linha vertical o MENUBREAK - posiciona o item numa nova coluna
As opções GRAYED e INACTIVE NÃO podem ser usadas em conjunto.
Exemplo de um recurso de menu lanchonete MENU BEGIN MENUITEM "&Sopa", 100 MENUITEM "S&alada", 101 POPUP "&Entradas" BEGIN MENUITEM "&Peixe", 200 MENUITEM "&Frango", 201, CHECKED POPUP "&Afrodisíacos" BEGIN MENUITEM "Sopa de &Piranha", 300 MENUITEM "&Amendoim", 301 END END MENUITEM "So&bremesa", 103 END
Menu padrão da classe Para que um menu seja o menu padrão de uma classe, é preciso incluí-lo na estrutura WNDCLASSEX. A partir daí, toda janela que for criada baseada na classe definida por esta estrutura estará associada a este menu padrão da classe - o menu default.
Adicionando um menu padrão A esta altura do campeonato você já está careca de saber que existe um modelo de criação de janelas. Vou apenas destacar a inclusão do menu "lanchonete". ... .DATA NomeClasse db "MenuPadrao", 0 TituloJanela db "Menu Malandro", 0 NomeMenu db "lachonete", 0 // O nome do menu no arquivo de recursos .CODE inicio: ... gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD ... mov ej.cbSize, SIZEOF WNDCLASSEX mov ej.style, CS_HREDRAW or CS_VREDRAW mov ej.lpfnWndProc, OFFSET gerenteMensagem mov ej.cbClsExtra, NULL mov ej.cbWndExtra, NULL push mInst pop ej.hInstance invoke LoadIcon, NULL, IDI_WINLOGO mov ej.hIcon, eax mov ej.hIconSm, eax invoke LoadCursor, NULL, IDC_ARROW mov ej.hCursor, eax mov ej.hbrBackground, COLOR_WINDOW+1 mov ej.lpszMenuName, OFFSET NomeMenu mov ej.lpszClassName, OFFSET NomeClasse invoke RegisterClassEx, ADDR ej ...
Menu específico de um objeto Um dos parâmetros da função CreateWindowEx é para indicar o menu associado à janela que está sendo criada, por isto mesmo, um menu particular. Este menu particular tem precedência sobre o menu padrão da classe. Em outras palavras, caso tenham sido definidos um menu padrão na classe e um menu específico ao criar um objeto desta classe, somente o menu específico é mostrado. Portanto, podemos definir um menu para a classe, um menu para o objeto ou os dois. Só para relembrar, aqui vai a função: HWND CreateWindowEx( DWORD dwExStyle, // estilo ampliado LPCTSTR lpClassName, // ponteiro para o nome da classe LPCTSTR lpWindowName, // ponteiro para o nome da janela DWORD dwStyle, // estilo da janela
int x, // posição horizontal da janela int y, // posição vertical da janela int nWidth, // largura da janela int nHeight, // altura da janela HWND hWndParent, // manipulador para a janela-mãe ou proprietária HMENU hMenu, // manipulador para o menu HINSTANCE hInstance, // manipulador para a instância do aplicativo LPVOID lpParam // ponteiro para dados para a criação da janela );
Adicionando um menu específico ... .DATA NomeClasse db "MenuPadrao",0 TituloJanela db "Menu Malandro",0 NomeMenu db "lachonete", 0 // O nome do menu no arquivo de recursos .DATA? mMenu HMENU ? .CODE inicio: ... gerenteJanela proc mInst:DWORD, mInstAnt:DWORD, linhaCmd:DWORD, Mostra:DWORD ... invoke LoadMenu, mInst, OFFSET NomeMenu mov mMenu, eax invoke CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, mMenu, mInst, NULL ...
Gerenciando as seleções de itens do menu Quando o usuário seleciona um item de menu, o sistema gera uma mensagem WM_COMMAND cujo parâmetro wParam contém o identificador (ID) deste item. O identificador ocupa apenas a palavra (word) menos significativa de wParam (Se você tiver dúvidas, leia mais sobre palavra menos significativa em "Registradores"). Podemos então armazenar a ID do item de menu em AX, compará-la com as IDs definidas para cada item no arquivo de recursos e definir o procedimento adequado. Fica mais prático definir constantes que correspondam às IDs de cada item de menu. É claro que seus valores precisam ser os definidos no arquivo de recursos. ... .CONST
mnID_sopa equ 100 mnID_salada equ 101 mnID_peixe equ 200 mnID_frango equ 201 mnID_piranha equ 300 mnID_amendoim equ 301 mnID_sobremesa equ 103 ... .CODE inicio: ... gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ... .IF uMsg == WM_COMMAND mov eax, wParam .IF ax == mnID_sopa ... .ELSEIF ax == mnID_salada ... ... .ENDIF .ELSEIF ...
Janela com menu padrão
Resumindo:
Menus são recursos descritos em arquivos de recursos de acordo com a sintaxe da Resource Script Language. Cada item de menu precisa de um número identificador (ID) para que possa ser acessado. Menus podem ser associados a uma classe. São os menus padrão incluídos na estrutura WNDCLASSEX. Neste caso, todos os objetos criados a partir desta classe possuem o mesmo menu. Menus também podem estar associados a um objeto. São os menus referenciados no parÂmetro hMenu da função que cria o objeto (a
CreateWindowEx). Neste caso, mesmo que exista um menu padrão da classe, o menu mostrado é o do objeto. Para download: O arquivo tutNB08.zip (15 Kb) contém o código fonte e os exemplos deste tutorial. Está em Downloads / Tutoriais / Assembly Numaboa.
Índice do Artigo Menu malandro (masm) Deslocando a janela Todas as páginas Trabalhando com variáveis do sistema. Brinque com o usuário. Resolução de tela, coordenadas de janelas, data e hora do sistema são algumas das novidades usadas para criar uma janela que "foge" do cursor do mouse. Neste tutorial vamos fazer uma brincadeira com o usuário. Aproveitando o código fonte do tutorial anterior, o Criando menus em Assembly, vamos obter algumas informações do contexto (as assim chamadas variáveis do sistema) e fazer com que a janela "fuja do cursor do mouse". Veremos como obter a resoluçãoo de tela, o tamanho de uma janela, hora e data do sistema e a posição de uma janela na tela. Veremos também como interagir com estes dados para obter os resultados desejados.
O tipo da janela O código fonte base é o mesmo escrito para o tutorial "Criando menus em Assembly", assim como o arquivo de recursos. O arquivo de recursos não precisa ser alterado. Fazendo algumas pequenas modificações no código fonte, podemos fazer uma brincadeira com o usuário - escolhendo determinados itens do menu, a janela "foge". A primeira pequena modificação será feita na função CreateWindowEx: fixaremos o tamanho da janela em 300 x 200 pixels e tiraremos os elementos do canto superior direito da janela que permitem maximizar ou minimizar a janela. ... .DATA ... largJanela dd 300 altJanela dd 200 .CODE inicio: ... gerenteJanela proc mInst:HINSTANCE, mInstAnt:HINSTANCE,
linhaCmd:LPSTR, Mostra:DWORD ... INVOKE CreateWindowEx, NULL, ADDR NomeClasse, ADDR TituloJanela,\ WS_POPUPWINDOW or WS_CAPTION, CW_USEDEFAULT,\ CW_USEDEFAULT, largJanela, altJanela, NULL, NULL,\ mInst, NULL ... end inicio
Trocando o texto Primeiramente vamos trocar o texto que será mostrado ao usuário quando escolher algum item do menu. Use a imaginação e divirta-se. Minha sugestão não está "aquelas coisas", mas serve como exemplo: ... .DATA NomeClasse db "MenuPadrao",0 TituloJanela db "Menu Malandro",0 NomeMenu db "lanchonete",0 Texto_sopa db "SOPA de Letrinhas - Só para adultos",0 Texto_salada db "SALADA Brejeira - Temperada com besteira",0 Texto_peixe db "PEIXE Ecológico - Direto do brejão",0 Texto_frango db "FRANGO Sadio - Goleiro sarado",0 Texto_piranha db "Nananinanão... hehehe",0 Texto_sobremesa db "SOBREMESA Especial - Bala perdida",0 Texto_fim db "Até outra hora...",0
Trocando caixas de mensagens por texto na tela Ao invés de utilizar a função MessageBox para criar caixas de mensagem para indicar o item de menu escolhido, vamos "pintar" o texto referente a cada item do menu na área cliente da janela. O procedimento já foi descrito em detalhes no tutorial Pintando Texto. A lógica é a seguinte:
Criamos uma variável global que recebe o endereço do texto referente ao item de menu selecionado. Este valor é obtido quando interceptamos a mensagem WM_COMMAND. No final da rotina do WM_COMMAND chamamos a função InvalidateRect, a qual força uma repintura da área cliente da janela, ou seja, invalida a área e envia uma mensagem WM_PAINT. Interceptamos a mensagem WM_PAINT e fazemos a pintura do texto indicado pelo endereço da variável global.
A coisa fica com esta cara: ... .DATA?
mInstancia HINSTANCE ? endTexto WPARAM ? ... gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL mCM:HDC LOCAL ps:PAINTSTRUCT LOCAL eRet: RECT ... .ELSEIF uMsg == WM_COMMAND mov eax, wParam .IF ax == mnID_fim invoke MessageBox, NULL, ADDR Texto_fim, OFFSET TituloJanela, MB_OK invoke DestroyWindow, hWnd .ELSEIF ax == mnID_sopa mov endTexto, OFFSET Texto_sopa .ELSEIF ax == mnID_salada mov endTexto, OFFSET Texto_salada .ELSEIF ax == mnID_peixe mov endTexto, OFFSET Texto_peixe .ELSEIF ax == mnID_frango mov endTexto, OFFSET Texto_frango .ELSEIF ax == mnID_sobremesa movendTexto, OFFSET Texto_sobremesa .ELSEIF ax == mnID_piranha mov endTexto, OFFSET Texto_piranha ... .ELSEIF ax == mnID_amendoim mov endTexto, OFFSET Texto_piranha ... .ENDIF invoke InvalidateRect, hWnd, NULL, TRUE .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR ps mov mCM, eax invoke GetClientRect, hWnd, ADDR eRet invoke DrawText, mCM, endTexto, -1, ADDR eRet,\ DT_SINGLELINE or DT_CENTER or DT_VCENTER invoke EndPaint, hWnd, ADDR ps .ELSE ...
Se você quiser, neste ponto é possível compilar um executável e testá-lo. A janela ainda não "foge", porém as mensagens relativas aos itens de menu aparecem na área cliente da janela.
Criando a função que desloca a janela Os itens escolhidos para deslocarem a janela são "Sopa de Piranha" e "Amendoim". Quando o usuário os escolher, a janela deve se deslocar na tela para uma posição diferente. Para tanto, vamos declarar a função "foge". Esta função calcula as novas coordenadas do canto superior esquerdo da janela e fará com que assuma a nova posição.
Declarando a função foge A primeira providência será declarar o protótipo da função e, logo a seguir, declarar a pópria função. Vamos precisar do manipulador da janela para podermos alterar algumas das suas propriedades e, por isto, o único parâmetro da função será o manipulador: .386 .model flat,stdcall option casemap:none gerenteJanela proto :DWORD,:DWORD,:DWORD,:DWORD foge proto :DWORD ... .CODE inicio: ... foge proc hWnd:HWND ... foge endp end inicio
Calculando a resolução da tela Tudo bem. Queremos deslocar nossa janela pela tela, porém ainda não sabemos quantos pixels poderemos deslocá-la no sentido horizontal ou vertical por que desconhecemos quantos pixels estão disponíveis. A função que pode nos fornecer todas as medidas de elementos do sistema, entre elas a resolução de tela, é a GetSystemMetrics: int GetSystemMetrics( int nIndex // medida do sistema ou configuração desejada
);
O parâmetro nIndex, entre outros, pode ser SM_CXSCREEN. Neste caso, o valor de retorno é o número de pixels da horizontal (eixo X). Usando o SM_CYSCREEN obtemos o número de pixels na vertical (eixo Y). Como usaremos as coordenadas do canto superior esquerdo da janela para movimentá-la na tela, os pixels disponíveis na horizontal serão os do eixo X MENOS o número de pixels da largura da janela. Se não descontarmos os pixels da largura da janela corremos o risco de posicioná-la fora da tela (ou mostrá-la apenas parcialmente). Da mesma forma, os pixels disponíveis na vertical serão os do eixo Y MENOS o número de pixels da altura da janela. Os valores assim obtidos serão armazenados nas variáveis locais largMax e altMax. foge proc hWnd:HWND LOCAL largMax, altMax:DWORD invoke GetSystemMetrics, SM_CXSCREEN sub eax, largJanela mov largMax, eax invoke GetSystemMetrics, SM_CYSCREEN sub eax, altJanela mov altMax, eax ... foge endp end inicio
Determinando a posição atual da janela A posição atual de uma janela pode ser obtida através da função GetWindowRect. Esta função precisa do manipulador da janela e de uma estrutura RECT, onde serão armazenadas as coordenadas de tela da posição da janela cujo manipulador foi fornecido: BOOL GetWindowRect( HWND hWnd, // manipulador da janela LPRECT lpRect // endereço da estrutura RECT ); typedef struct _RECT { // rc LONG left; // esquerda LONG top; // superior LONG right; // direita LONG bottom; // inferior } RECT; foge proc hWnd:HWND LOCAL largMax, altMax:DWORD LOCAL retang:RECT invoke GetSystemMetrics, SM_CXSCREEN sub eax, largJanela mov largMax, eax invoke GetSystemMetrics, SM_CYSCREEN
sub eax, altJanela mov altMax, eax invoke GetWindowRect, ADDR ... foge endp end inicio
Obtendo a hora do sistema As posições que a janela deve ocupar são aleatórias e, para calculá-las, usaremos um expediente: a nova posição dependerá dos milésimos de segundo indicados pelo relógio do sistema, pois a possibilidade de repetição deste valor é muito remota. A função GetSystemTime não possui valor de retorno, mas preenche uma estrutura SYSTEMTIME com a hora do sistema: VOID GetSystemTime( LPSYSTEMTIME lpSystemTime // endereço da estrutura SYSTEMTIME );
A estutura SYSTEMTIME possui os seguintes membros: typedef struct _SYSTEMTIME { // st WORD wYear; // ano WORD wMonth; // mês WORD wDayOfWeek; // dia da semana WORD wDay; // dia WORD wHour; //hora WORD wMinute; // minutos WORD wSecond; // segundos WORD wMilliseconds; milésimos de segundo } SYSTEMTIME;
Para obter os dados desejados, precisamos declarar uma variável local do tipo SYSTEMTIME e chamar a função GetSystemTime que tenha como parâmetro o endereço da nossa variável: foge proc LOCAL LOCAL LOCAL
hWnd:HWND largMax, altMax:DWORD retang:RECT system_time:SYSTEMTIME
invoke GetSystemMetrics, SM_CXSCREEN sub eax, largJanela mov largMax, eax invoke GetSystemMetrics, SM_CYSCREEN sub eax, altJanela mov altMax, eax invoke GetWindowRect, ADDR invoke GetSystemTime, ADDR system_time
... foge endp end inicio
O valor que desejamos será encontrado em system_time.wMilliseconds.
Calculando a nova posição da janela Teremos que calcular o novo valor de X (posição horizontal na tela) e de Y (posição vertical na tela). A posição atual está no membro da estrutura retang.left e retang.top. Se somarmos o valor de retang.left (posição X atual da janela) aos milésimos de segundo que estão em system_time.wMilliseconds, podem ocorrer duas situações: o novo valor de X ou é maior ou é menor do que a largMax calculada. Caso seja maior, se movermos a janela para este ponto, ela ficará parcialmente visível ou então fora da tela. Faremos uso da sintaxe de alto nível do MASM para criar um loop: se o novo valor de X for maior que largMax, diminuímos este valor de X (novo X - largMax) tantas vezes quantas forem necessárias para que o novo valor de X seja menor que largMax. Assim que o novo valor de X for menor que largMax, saímos do loop e guardamos o valor de X na variável local novoX. O mesmo raciocínio se aplica para o cálculo do novo valor de Y. foge proc LOCAL LOCAL LOCAL LOCAL
hWnd:HWND largMax, altMax:DWORD retang:RECT system_time:SYSTEMTIME novoX, novoY:DWORD
invoke GetSystemMetrics, SM_CXSCREEN sub eax, largJanela mov largMax, eax invoke GetSystemMetrics, SM_CYSCREEN sub eax, altJanela mov altMax, eax invoke GetWindowRect, ADDR invoke GetSystemTime, ADDR system_time mov ax, system_time.wMilliseconds add eax, retang.left .WHILE TRUE .BREAK .IF (eax < largMax) sub eax, largMax .ENDW mov novoX, eax mov ax, system_time.wMilliseconds add eax, retang.top .WHILE TRUE .BREAK .IF (eax < altMax) sub eax, altMax
.ENDW mov novoY, eax ... foge endp end inicio
Note que transferimos para AX (e não EAX) o valor dos milisegundos. É que a estrutura system_time foi criada a partir do modelo SYSTEMTIME, cujos membros são todos do tipo WORD (16 bits). Caso você tentar transferir para EAX (32 bits) o valor de qualquer membro de system_time (16 bits), o assembler indicará o erro "instruction operands must be the same size", ou seja, os operandos da instrução precisam ser do mesmo tamanho.
Mudando a posição da janela De posse de todos os parâmetros necessários para mudar a posição da janela, agora podemos chamar a função MoveWindow: BOOL MoveWindow( HWND hWnd, // manipulador da janela int X, // posição horizontal int Y, // posição vertical int nWidth, // largura int nHeight, // altura BOOL bRepaint // indicador de repintura );
O parâmetro hWnd veio como parâmetro para nossa função "foge". Os parâmetros X e Y são os nossos novoX e novoY. A largura e a altura não serão modificadas, portanto usaremos as variáveis globais largJanela e altJanela. O indicador de repintura será TRUE (verdadeiro) para forçar o sistema a enviar uma mensagem WM_PAINT, devidamente interceptada e onde faremos nossa pintura personalizada. Depois disto, basta um ret para terminarmos a função foge: foge proc LOCAL LOCAL LOCAL LOCAL
hWnd:HWND largMax, altMax:DWORD retang:RECT system_time:SYSTEMTIME novoX, novoY:DWORD
invoke GetSystemMetrics, SM_CXSCREEN sub eax, largJanela mov largMax, eax invoke GetSystemMetrics, SM_CYSCREEN sub eax, altJanela mov altMax, eax invoke GetWindowRect, ADDR invoke GetSystemTime, ADDR system_time
mov ax, system_time.wMilliseconds add eax, retang.left .WHILE TRUE .BREAK .IF (eax < largMax) sub eax, largMax .ENDW mov novoX, eax mov ax, system_time.wMilliseconds add eax, retang.top .WHILE TRUE .BREAK .IF (eax < altMax) sub eax, altMax .ENDW mov novoY, eax invoke MoveWindow, hWnd, novoX, novoY, largJanela, altJanela, TRUE ret foge endp end inicio
Índice do Artigo Assembly - Fontes (masm) Definindo a fonte Todas as páginas Mudando o visual do Menu Malandro. Como trabalhar com fontes e cores. O uso de macros para facilitar a programação. Neste tutorial vamos fazer a brincadeira da janela "fujona" ficar um pouco mais colorida. O projeto é o mesmo do tutorial anterior, o " Menu Malandro", no qual incluiremos algumas funções que lidam especificamente com fontes.
O que são fontes No Windows, uma fonte é uma coleção de caracteres e símbolos gráficos que compartilham um estilo (ou um design). Os três elementos mais importantes deste estilo são o tipo, o estilo e o tamanho. O termo TIPO refere-se às características específicas dos caracteres e símbolos da fonte, como a largura das linhas finas e grossas que compõem os caracteres e a presença ou ausência de serifs. Um serif é uma pequena linha que serve como "acabamento" de linhas não conectadas (por exemplo, as pequenas linhas horizontais nos "pés" da letra M
na fonte Times New Roman - M). Uma fonte sem serif geralmente é denominada sansserif. O termo ESTILO refere-se ao peso (espessura das linhas) e à inclinação de uma fonte. O peso pode variar de super-leve até super-pesado, ou seja: fino (Thin), extra leve (Extralight), leve (Light), normal (Normal), médio (Medium), semi-negrito (Semibold), negrito (Bold), extra-negrito (Extrabold) e pesado (Heavy). A inclinação pode ser romana, oblíqua ou itálica. Os caracteres de uma fonte romana ficam alinhados na vertical (diz-se que ficam "em pé"). Os de uma fonte oblíqua são inclinados artificialmente. Esta inclinação é obtida podando-se os caracteres de uma fonte romana. Os caracteres de uma fonte itálica são inclinados autênticos e aparecem como foram desenhados. No Windows, o TAMANHO das fontes é um valor impreciso. Geralmente pode ser determinado medindo-se a distância da base de um "g" minúsculo até o topo de um "M" maiúsculo adjacente. O tamanho de uma fonte é especificado em uma unidade chamada ponto. De acordo com o sistema de pontos idealizado por Pierre Simon Fournier, um ponto corresponde a 0.013837 polegadas (ou 0,3514598 milímetros). A grosso modo, isto corresponde a 1/72 de polegada (ou 1/3 de milímetro). O Windows organiza as fontes por família. Uma FAMÍLIA é um conjunto de fontes que possuem a mesma espessura e características serif. Existem cinco famílias com nomes específicos. Um sexto nome, o "Dontcare", permite que um aplicativo utilize a fonte padrão. A tabela a seguir descreve os nomes das famílias de fontes:
Nome da Família Decorative Dontcare Modern Roman Script Swiss
Descrição Denomina uma fonte nobre. Um exemplo é a Old English. Um nome de família genérico. É usado quando não há informações sobre a fonte ou quando as informações não têm importância. Especifica uma fonte monospace com ou sem serifs. Fontes monospace geralmente são modernas. Exemplos: Pica, Elite, and Courier New®. Fonte proporcional com serifs. Um exemplo é a Times New Roman. Especifica fontes desenhadas para se parecerem com escrita manual. São exemplos a Script e a Cursive. Para fontes proporcionais sem serifs. Um exemplo é a Arial.
Estes nomes de famílias correspondem a constantes do Windows: FF_DECORATIVE, FF_DONTCARE, FF_MODERN, FF_ROMAN, FF_SCRIPT e FF_SWISS. Um aplicativo usa estas constantes quando cria, seleciona ou obtém informações sobre uma fonte. As fontes de uma mesma família distinguem-se pelo tamanho (10 pontos, 24 pontos, etc) e pelo estilo (regular, itálico e assim por diante).
As cores no Windows Esta pequena introdução ao sistema RGB é necessária para podermos criar o código fonte do exemplo.
No Windows, as cores são expressas pela sua composição de vermelho, verde e azul. É o sistema RGB, onde R = Red (vermelho), G = Green (verde) e B = Blue (azul). Os valores para estas cores básicas podem variar de 0 a 255, onde 0 é nada e 255 é o máximo. Se quisermos indicar vermelho puro no máximo a composição será R = 255, G = 0 e B = 0. Branco é o resultado da composição do máximo das três cores básicas e indicado por 255,255,255. Preto, por sua vez, é a ausência de todas as cores - 0,0,0.
Preparando as cores Use o código fonte do tutorial Menu Malandro como base para o código fonte deste tutorial. Antes de iniciarmos a criação e o uso de fontes, vamos preparar uma macro cuja função será devolver o valor correspondente a uma cor em RGB no registrador EAX. Mas o que são macros? Uma das grandes vantagens do MASM é que ele é um "MACRO assembler", cuja tradução literal é "construtor de macros". Um macro assembler pre-processa o texto antes que o assembler leia e compile o código. Esta característica propicia ao programador a facilidade de escrever procedimentos MACRO que ampliam ou modificam o código fonte. Com o uso de macros, o código fonte pode ser escrito de uma forma mais rápida e confiável. MACROS são as armas secretas na produção de código, pois realizam tarefas específicas na manipulação de código. Podem ser usadas com vários propósitos, desde uma simples substituição de texto até loops de expansões complexas de código repetitivo. Vamos fazer da macro RGB o nosso exemplo: ... RGB MACRO red, green, blue xor eax, eax mov ah, blue shl eax, 8 mov ah, green mov al, red ENDM .DATA ...
Cada vez que usarmos RGB seguido dos valores da intensidade das cores, por exemplo RGB 255,96,128 , o texto destacado em negrito será substituído pelo texto da macro definido acima. O que esta macro faz é o seguinte: 1. Zera o registrador EAX com a instrução XOR EAX,EAX. 2. Transfere para o byte mais significativo (8 bits) de EAX o valor da cor azul (EAX = 00 00 80 00). 3. Desloca os bits do byte mais significativo 8 posições para a esquerda (EAX = 00 80 00 00). 4. Transfere para o byte mais significativo de EAX o valor da cor verde (EAX = 00
80 60 00). 5. Transfere para o byte menos significativo de EAX o valor da cor vermelha (EAX = 00 80 60 FF). Esta macro tem uma ampla aplicação na programação assembly. Seu uso é tão frequente que é bom mantê-la sempre à mão.
Definindo a fonte a ser usada Para poder usar determinada fonte precisamos criá-la e selecioná-la. Só depois destes dois passos é que a fonte se encontra ativa e pronta para uso.
Criando uma fonte lógica Para poder criar uma fonte lógica utiliza-se uma das funções da API, a CreateFont. Esta função, dentre as funções do Windows, é a que possui o maior número de parâmetros: Função da API
HFONT CreateFont( int nHeight, // altura lógica da fonte int nWidth, // largura lógica média dos caracteres int nEscapement, // ângulo de escape int nOrientation, // ângulo de orientação da linha base int fnWeight, // peso da fonte DWORD fdwItalic, // indicador do atributo itálico DWORD fdwUnderline, // indicador do atributo sublinhado DWORD fdwStrikeOut, // indicador do atributo riscado (ou tachado) DWORD fdwCharSet, // identificador do conjunto de caracteres DWORD fdwOutputPrecision, // precisão de saída DWORD fdwClipPrecision, // precisão de corte DWORD fdwQuality, // qualidade da saída DWORD fdwPitchAndFamily, // pitch (distanciamento) e família LPCTSTR lpszFace // ponteiro para a string do nome do tipo );
nHeight é a altura dos caracteres. Zero indica a altura padrão. nWidth é a largura dos caracteres. Zero indica a largura padrão ( que o Windows ajusta de acordo com a altura dos caracteres). nEscapement especifica a orientação do caractere seguinte em relação ao anterior, em décimos de grau. Normalmente indica-se 0 (zero). Se for 900, os caracteres vão sendo colocados um acima do outro; se for 1800, vão sendo colocados em ordem reversa (da direita para a esquerda); se for 2700, são colocados de cima para baixo. nOrientation indica a rotação que os caracteres devem sofrer, em décimos de grau. Normalmente indica-se 0 (zero). Se for 900, rodam 90 graus e ficam "deitados"; se for 1800, ficam de "cabeça para baixo"; etc.
fnWeight indica a espessura dos caracteres: FW_DONTCARE (0), FW_THIN (100), FW_EXTRALIGHT ou FW_ULTRALIGHT (200), FW_LIGHT (300), FW_NORMAL ou FW_REGULAR (400), FW_MEDIUM (500), FW_SEMIBOLD ou FW_DEMIBOLD (600), FW_BOLD (700), FW_EXTRABOLD ou FW_ULTRABOLD (800) e FW_HEAVY ou FW_BLACK (900). fdwItalic: zero indica normal, qualquer outro valor indica o estilo itálico. fdwUnderline: zero indica normal, qualquer outro valor indica caracteres sublinhados. fdwStrikeOut: zero indica normal, qualquer outro valor indica caracteres riscados. fdwCharSet: o conjunto de caracteres da fonte. Normalmente, para obter caracteres com til e cedilha, usamos ANSI_CHARSET. fdwOutputPrecision: especifica o quanto a fonte selecionada precisa se aproximar das características que desejamos. Normalmente deveria ser OUT_DEFAULT_PRECIS, que define um mapeamento padrão da fonte. fdwClipPrecision: define a precisão de corte dos caracteres que estão parcialmente fora da região de corte. Geralmente se usa CLIP_DEFAULT_PRECIS, que define o comportamento de corte padrão. fdwQuality: a qualidade da saída define o quanto a GDI precisa se preocupar em adequar os atributos da fonte lógica aos atributos da fonte física. Há três escolhas possíveis: DEFAULT_QUALITY (média), PROOF_QUALITY (alta) e DRAFT_QUALITY (baixa). fdwPitchAndFamily: indica o distanciamento entre os caracteres e a família da fonte. Podem ser associados através de OR. lpszFace: ponteiro para a string que contém o nome da fonte que queremos usar.
O valor de retorno desta função, como sempre no registrador EAX, é o manipulador para a fonte lógica criada. Tá certo que não ficou muito claro... é coisa demais mesmo. Talvez, fazendo uso da função, fique um pouco mais fácil. Vamos declarar uma variável global com o nome da fonte escolhida, uma variável local que receberá o manipulador da nova fonte e, na interceptação da mensagem WM_PAINT do procedimento gerenteMensagem, vamos criar a fonte Comic Sans MS. Se você não tiver esta fonte instalada no seu micro, escolha qualquer outra. Além disto, precisamos referenciar a biblioteca GDI, à qual pertencem a maioria das funções que serão adicionadas neste exemplo. ... include include include include
\masm32\include\windows.inc \masm32\include\user32.inc \masm32\include\kernel32.inc \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\gdi32.lib ... .DATA ...
Texto_fim db "Até outra hora...",0 NomeFonte db "Comic Sans MS",0 .CODE inicio: ... gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ... LOCAL mFonte:HFONT .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR ps mov mCM, eax invoke CreateFont, 0, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH or FF_DONTCARE, ADDR FontName mov mFonte, eax ...
Selecionando a fonte lógica Uma vez criada a fonte lógica, podemos substituir a fonte do contexto de dispositivo pela recém criada. A função SelectObject faz o serviço para nós e retorna o manipulador do objeto substituído. Vamos guardar este manipulador numa variável local. Após usarmos a nova fonte, este manipulador servirá para reverter a substituição da fonte (sempre é bom voltar para o contexto de dispositivo original). Função da API
HGDIOBJ SelectObject( HDC hdc, // manipulador do contexto de dispositivo HGDIOBJ hgdiobj // manipulador do objeto ); gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ... LOCAL mFonte:HFONT LOCAL mFonteOriginal:HFONT .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR ps mov mCM, eax invoke CreateFont, 0, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH or FF_DONTCARE, ADDR FontName mov mFonte, eax invoke SelectObject, mCM, mFonte mov mFonteOriginal, eax ...
Definindo a cor do texto Chegou a hora de usarmos nossa macro RGB. Sabendo que RGB devolve a cor no registrador EAX, o valor contido neste registrador será um dos parâmetros para a função SetTextColor. Em seguida pintamos o texto como habitual e, logo depois, retornamos a fonte original para o contexto de dispositivo. Função da API
COLORREF SetTextColor( HDC hdc, // manipulador do contexto de dispositivo COLORREF crColor // cor do texto ); gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ... LOCAL mFonte:HFONT LOCAL mFonteOriginal:HFONT .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR ps mov mCM, eax invoke CreateFont, 0, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH or FF_DONTCARE, ADDR FontName mov mFonte, eax invoke SelectObject, mCM, mFonte mov mFonteOriginal, eax RGB 255, 0, 0 invoke SetTextColor, mCM, eax invoke GetClientRect, hWnd, ADDR eRet invoke DrawText, mCM, endTexto, -1, ADDR eRet, DT_SINGLELINE or DT_CENTER or DT_VCENTER invoke EndPaint, hWnd, ADDR ps invoke SelectObject, mCM, mFonteOriginal ...
Destruindo a fonte lógica Todos objetos do contexto de dispositivo, quando são criados, consomem recursos do sistema. Criamos uma fonte lógica para ser usada no nosso contexto de dispositivo e, quando não precisamos mais dela, o correto é destruí-la para liberar os recursos do sistema. A função DeleteObjet elimina objetos do tipo pincel, fonte, bitmap, região, etc. É claro que, depois de destruídos, os manipuladores destes objetos deixam de ser válidos. Função da API
BOOL DeleteObject(
HGDIOBJ hObject // manipulador do objeto gráfico );
Já que estamos fazendo faxina, não custa nada voltar a cor do texto para a original. Basta guardar a cor original numa variável local antes de trocar a cor do texto para, posteriormente, reverter as cores. A função que nos permite obter a cor do texto atual é a GetTextColor. gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ... LOCAL mFonte:HFONT LOCAL mFonteOriginal:HFONT LOCAL CorTexto:DWORD .ELSEIF uMsg == WM_PAINT invoke BeginPaint, hWnd, ADDR ps mov mCM, eax invoke CreateFont, 0, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH or FF_DONTCARE, ADDR FontName mov mFonte, eax invoke SelectObject, mCM, mFonte mov mFonteOriginal, eax invoke GetTextColor, mCM mov CorTexto, eax RGB 255, 0, 0 invoke SetTextColor, mCM, eax invoke GetClientRect, hWnd, ADDR eRet invoke DrawText, mCM, endTexto, -1, ADDR eRet, DT_SINGLELINE or DT_CENTER or DT_VCENTER invoke EndPaint, hWnd, ADDR ps invoke SelectObject, mCM, mFonteOriginal invoke SetTextColor, CorTexto invoke DeleteObject, mFonte .ELSE ...
A nova aparência do texto Aqui terminamos a reforma do código fonte. Gere o executável e verifique o resultado.
Menu malandro com uma fonte especial para o texto na área principal
O exemplo fontes II No pacote para download (está na seção downloads / tutoriais / assembly numaboa) existe um segundo exemplo cujo código fonte está em fontes2.asm. A direrença é a seguinte: para cada item de menu o texto é apresentado numa cor diferente. Para isto foi criada uma variável global corAtual, do tipo DWORD, não inicializada, que contém o valor da cor desejada. Na macro RGB foi incluída uma instrução que inicializa corAtual: RGB MACRO red, green, blue xor eax, eax mov ah, blue shl eax, 8 mov ah, green mov al, red mov corAtual, eax ENDM
A macro é utilizada apenas no processamento da mensagem WM_COMMAND: gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ... .ELSEIF uMsg == WM_COMMAND ... .ELSEIF ax == mnID_sopa mov endTexto, OFFSET Texto_sopa RGB 255,0,0 .ELSEIF ax == mnID_salada mov endTexto, OFFSET Texto_salada RGB 0,128,0 .ELSEIF ...
As instruções em WM_PAINT que trocavam a cor do texto RGB 255, 0, 0 invoke SetTextColor, mCM, eax
serão substituídas por: invoke SetTextColor, mCM, corAtual
Índice do Artigo Dialog Box (masm) Criando a janela Gerenciando o menu Fechando a janela Todas as páginas Uma dialog box como janela principal. Facilitando a vida com o uso de classes já definidas e gerenciamento automático. Adicionando componentes e economizando programação. Uma caixa de diálogo nada mais é do que uma janela normal que foi desenhada para trabalhar com controles do tipo caixas de edição e botões. Os controles, por sua vez, são apenas janelas-filha. A classe de janelas dialog box foi criada para diminuir o trabalho dos programadores e oferece uma porção de facilidades tipo "prato pronto". O pessoal da microsoft chamou esta janela "especial" de CAIXA de diálogo (dialog box), mas eu prefiro o termo JANELA de diálogo. Neste tutorial usaremos uma janela de diálogo como janela principal.
Um pouco de teoria O Windows possui um "gerente de janelas de diálogo" que administra a maior parte da lógica do teclado, como acionar o botão default quando a tecla [Enter] for pressionada ou mudar o foco de entrada quando a tecla [Tab] for pressionada. As janelas de diálogo são utilizadas principalmente como dispositivos de entrada e saída. Como o gerenciamento fica por conta do Windows, elas funcionam como um espécie de caixa preta: não precisamos saber como uma janela de diálogo funciona internamente, basta saber como interagir com ela. Caso você não saiba, este é um dos princípios da programação orientada a objetos, denominado ocultação de informações por encapsulamento. Se você acredita no gerente de diálogos... basta usar a dialog box. Vamos às mordomias. Normalmente, se pusermos controles (janelas-filha) numa janela normal, precisamos produzir estas janelas-filha como subclasses da janela-mãe, ou seja, precisamos derivar as propriedades da janela-mãe. Além disto, toda a lógica do teclado fica por nossa conta. É muita mão de obra. É muito mais fácil e cômodo deixar o gerente do sistema trabalhar pra gente.
Janelas de diálogo são definidas como um recurso, da mesma maneira que menus. Podemos escrever um modelo com as características e os controles da janela de diálogo e depois compilar o código fonte do recurso com um editor de recursos. É claro que podemos produzir o código fonte "na unha", mas é um processo muito chato. Usando um editor de recursos podemos construir nossa janela de diálogo num ambiente visual clicar e arrastar botões é mais fácil do que calcular suas coordenadas. Sugestões de editores de recurso você encontra no tutorial "Usando Recursos". Existem dois tipos de janelas de diálogo: modal e não modal. Uma janela de diálogo não modal permite que o foco seja transferido para outra janela; janelas modais tratam o foco de forma especial. Existem dois subtipos da janela de diálogo modal: modal de aplicativo e modal de sistema. Uma modal de aplicativo não permite que se mude o foco para outra janela do mesmo aplicativo, mas permite que se mude o foco para outra janela de OUTRO aplicativo. Uma modal de sistema nunca permite a mudança de foco ela precisa ser fechada antes.
Os recursos da janela de diálogo O código fonte do arquivo de recursos do nosso exemplo contém o modelo de janela de diálogo e um pequeno menu: rsrc.rc
#include "resource.h" #define #define #define #define
IDC_EDIT 3000 IDC_BUTTON 3001 IDC_EXIT 3002 MenuDaqui 3003
#define IDM_pegar 32000 #define IDM_limpar 32001 #define IDM_sair 32002 DLGNumaBoa DIALOG 10, 10, 205, 48 STYLE DS_MODALFRAME | 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | WS_3DLOOK CAPTION "Testando o Gerente" MENU MenuDaqui BEGIN EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT DEFPUSHBUTTON "Diga &Oi", IDC_BUTTON, 141,10,52,13 PUSHBUTTON "&Sair", IDC_EXIT, 141,26,52,13 END MenuDaqui MENU BEGIN POPUP "Testar Controles" BEGIN MENUITEM "Pegar Texto", mnID_pegar MENUITEM "Limpar Texto", mnID_limpar
MENUITEM SEPARATOR MENUITEM "&Sair", mnID_sair END END
Foi usado um #include referenciando o arquivo resource.h. Este arquivo contém as definições necessárias para dar suporte às constantes nominadas como WS_SYSMENU, WS_VISIBLE, etc. Em relação ao modelo da janela de diálogo:
O nome da classe: DLGNumaBoa. Palavra chave que identifica o tipo da janela: DIALOG. Medidas em unidades dialog box (não são pixels): 10,10,205,48 Palavra chave para indicar o estilo: STYLE. Composição com OR (|) de todos os estilos desejados: DS_MODALFRAME | ... Texto da barra de título: CAPTION "Testando o Gerente". O menu da janela, definido no mesmo arquivo de recursos: MENU MenuDaqui Início da definição dos controles: BEGIN. Caixa de Edição: EDITTEXT Identificador, Medidas, Opções Botão default: DEFPUSHBUTTON Texto no botão, Identificador, Medidas Botão: PUSHBUTTON Texto no Botão, Identificador, Medidas Fim da definição de controles: END
Não se esqueça de que o arquivo de recursos precisa ser compilado para poder ser incorporado ao executável! Note que o modelo da janela de diálogo possui um menu, o MenuDaqui, que também precisa ser definido no mesmo arquivo de recursos. Se tiver dúvidas quanto ao modelo do menu, reveja o tutorial "Criando menus em Assembly".
Criando a janela dialog box Quando criamos a janela "tradicional", logo após obtermos o manipulador da instância, chamamos o gerente da Janela (o procedimento gerenteJanela) que, entre outras tarefas, se encarrega de criar a janela principal. Acontece que sabemos que a dialog box possui um gerente próprio, que fica de plantão para nos atender. Basta requisitar seus serviços através de CreateDialogParam (se quisermos uma janela não modal) ou de DialogBoxParam (se quisermos uma janela modal). No caso, como queremos uma janela modal, só precisamos substituir a linha invoke gerenteJanela do modelo usado até agora por invoke DialogBoxParam: função da API
HWND CreateDialogParam( HINSTANCE hInstance, LPCTSTR lpTemplateName, HWND hWndParent, DLGPROC lpDialogFunc, LPARAM dwInitParam ); função da API
INT_PTR DialogBoxParam(
HINSTANCE hInstance, LPCTSTR lpTemplateName, HWND hWndParent, DLGPROC lpDialogFunc, LPARAM dwInitParam ); dialog.asm
... .DATA NomeClasse db "DLGNumaBoa",0 // precisa ser o nome dado no recurso .CODE inicio: invoke GetModuleHandle, NULL mov mInstancia, eax invoke DialogBoxParam, mInstancia, ADDR NomeClasse, NULL, ADDR gerenteMensagem, NULL invoke ExitProcess,0 end inicio
Gerenciando as mensagens recebidas Como não fomos nós que definimos a classe DLGNumaBoa, e sim o gerente de diálogos, precisamos apresentar-lhe nosso gerente de mensagens - que ele não conhece porque é "nosso funcionário". Nós indicamos gerenteMensagem na função DialogBoxParam e precisamos "entregar a ficha" do gerente de mensagens ao gerente de diálogos. Fazemos isto entregando o protótipo da função: dialog.asm
... includelib \masm32\lib\kernel32.lib gerenteMensagem proto :DWORD, :DWORD, :DWORD, :DWORD ...
Determinando o controle com o foco inicial Assim que a janela de diálogo for mostrada na tela, queremos que a caixa de edição tenha o foco. Nosso objetivo pode ser alcançado se interceptarmos a mensagem WM_INITDIALOG, que é a "prima próxima" da mensagem WM_CREATE da janela "comum". Como pretendemos alterar uma das propriedades da caixa de edição, é preciso fornecer ao gerente de mensagens o identificador (ID) da mesma. Este identificador, como todos os dos outros controles da janela de diálogo, é o definido no arquivo de recursos. Para obter o manipulador da caixa de edição basta chamar a função GetDlgItem com o identificador do controle e, para dirigir o foco para um controle, basta chamar a função SetFocus com o manipulador do controle:
função da API
HWND GetDlgItem( HWND hDlg, // manipulador da janela de diálogo int nIDDlgItem // identificador do controle ); dialog.asm
... .CONST IDC_EDIT equ 3000 IDC_BUTTON equ 3001 IDC_EXIT equ 3002 IDM_pegar equ 32000 IDM_limpar equ 32001 IDM_sair equ 32002 ... gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg == WM_INITDIALOG invoke GetDlgItem, hWnd, IDC_EDIT invoke SetFocus, eax .ELSEIF ...
Gerenciando os itens do menu A mensagem WM_COMMAND é enviada quando o usuário seleciona um item de comando do menu, quando um controle envia uma mensagem de notificação para a janela-mãe ou quando uma tecla aceleradora é traduzida. O formato da mensagem WM_COMMAND é: WM_COMMAND wNotifyCode = HIWORD(wParam); // código de notificação wID = LOWORD(wParam); // identificador do item, controle ou acelerador hwndCtl = (HWND) lParam; // manipulador do controle
O parâmetro wParam é um valor de 32 bits, ou seja, possui 4 bytes de 8 bits. Os dois bytes mais à esquerda formam o word mais significativo (high word) e os dois à direita formam o word menos significativo (low word). Tratando os words em separado, o parâmetro wParam contém dois valores: o código de notificação e o identificador do item, controle ou tecla aceleradora. Veja abaixo para entender melhor ou dê uma olhada no texto de apoio "O que são registradores" e "Assembly e Registradores". | 31 a 24 23 a 16 | 15 a 8 | | byte mais | | significativo | .. word mais significativo ..| | (código de notificação) | .... word menos | (identificador do controle) | |
| | |
7 a 0 | byte menos | significativo | | significativo ... | | |
| .......................... DWORD ............................... |
wNotifyCode: word mais significativo de wParam. Contém o código de notificação se a mensagem for de um controle. É 1 se a mensagem for de um acelerador e 0 (zero) se a mensagem for de um menu. wID: word menos significativo de wParam. Contém o identificador do item de menu, controle ou acelerador. hwndCtl: valor de lParam. Identifica o controle remetente se a mensagem for de um controle. Caso contrário, o valor é NULL.
De posse destas informações, sabemos que uma mensagem veio de um item de menu somente quando lParam de uma mensagem WM_COMMAND for igual a NULL. Vai daí que... precisamos interceptar a mensagem WM_COMMAND, testar se o parâmetro lParam é NULL e testar cada um dos itens do menu. O identificador do item do menu se encontra no word menos significativo de EAX, ocupando os primeiros 16 bits, por isso usamos AX (e não EAX). dialog.asm
... gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg == WM_INITDIALOG invoke GetDlgItem, hWnd, IDC_EDIT invoke SetFocus, eax .ELSEIF uMsg == WM_COMMAND mov eax, wParam .IF lParam == NULL .IF ax == IDM_pegar ...
Item do menu "Pegar Texto" Quando o item selecionado for "Pegar Texto", cujo ID é IDM_pegar, queremos que o texto que esteja na caixa de edição seja mostrado numa caixa de mensagem. Para obter o texto da caixa de edição utilizamos a função GetDlgItemText, para mostrar o texto usamos a função MessageBox. A última você já conhece do tutorial "Message Box". função da API
UINT GetDlgItemText( HWND hDlg, // manipulador da janela de diálogo int nIDDlgItem, // identificador do controle LPTSTR lpString, // endereço do buffer para o texto int nMaxCount // tamanho máximo da string ); função da API
int MessageBox( HWND hWnd, // manipulador da janela proprietária LPCTSTR lpText, // endereço do texto da message box
LPCTSTR lpCaption, // endereço do título da message box UINT uType // estilo da message box );
Note que vamos precisar de um buffer para armazenar o texto da caixa de edição, portanto vamos declará-lo na seção de dados não inicializados. Também vamos precisar do título da message box. Usaremos TituloJanela e modificamos apenas o texto: dialog.asm
... .DATA NomeClasse db "DLGNumaBoa",0 TituloJanela db "O que o gerente fez",0 .DATA? mInstancia HTINSTANCE ? buffer db 512 dip(?) ... gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg == WM_INITDIALOG invoke GetDlgItem, hWnd, IDC_EDIT invoke SetFocus, eax .ELSEIF uMsg == WM_COMMAND mov eax, wParam .IF lParam == NULL .IF ax == IDM_pegar invoke GetDlgItemText, hWnd, IDC_EDIT, ADDR buffer, 512 invoke MessageBox, hWnd, ADDR buffer, ADDR TituloJanela, MB_OK ... .ENDIF
Item do menu "Limpar Texto" Quando o item selecionado for "Limpar Texto", cujo ID é IDM_limpar, queremos apenas eliminar qualquer texto que possa estar na caixa de edição. Se trocarmos o texto existente por NULL, limpamos a caixa. Usamos a função SetDlgItemText: função da API
BOOL SetDlgItemText( HWND hDlg, // manipulador da janela de diálogo int nIDDlgItem, // identificador do controle LPCTSTR lpString // texto a ser incluído ); dialog.asm
.IF lParam == NULL .IF ax == IDM_pegar invoke GetDlgItemText, hWnd, IDC_EDIT, ADDR buffer, 512
invoke MessageBox, hWnd, ADDR buffer, ADDR TituloJanela, MB_OK .ELSEIF ax == IDM_pegar invoke SetDlgItemText, hWnd, IDC_EDIT, NULL ... .ENDIF
Item do menu "Sair" O identificador do item de menu "Sair" é IDM_sair. Quando for selecionado, queremos que uma caixa de mensagem diga "tchauzinho" e a janela de diálogo seja fechada. Precisamos inicializar a string do "tchauzinho" e utilizar a função EndDialog que destrói uma janela de diálogo modal, ou seja, dispensa o gerente de diálogo. função da API
BOOL EndDialog( HWND hDlg, // manipulador da janela de diálogo int nResult // valor de retorno ); dialog.asm
... .DATA NomeClasse db "DLGNumaBoa",0 TituloJanela db "O que o gerente fez",0 Adeus db "Tchauzinho",0 ... .IF lParam == NULL .IF ax == IDM_pegar invoke GetDlgItemText, hWnd, IDC_EDIT, ADDR buffer, 512 invoke MessageBox, hWnd, ADDR buffer, ADDR TituloJanela, MB_OK .ELSEIF ax == IDM_pegar invoke SetDlgItemText, hWnd, IDC_EDIT, NULL .ELSEIF ax == IDM_sair invoke MessageBox, hWnd, ADDR Adeus, ADDR TituloJanela, MB_OK invoke EndDialog, hWnd, NULL .ENDIF ...
Gerenciando o fechamento da janela Já que o fechamento da janela é possível de ser obtido através do item de manu "Sair", podemos aproveitar o código para fechar a janela quando o usuário clica no canto superior direito da janela no botãozinho [x]. Este botãozinho envia uma mensagem do tipo WM_CLOSE:
dialog.asm
gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg == WM_INITDIALOG invoke GetDlgItem, hWnd, IDC_EDIT invoke SetFocus, eax .IF uMsg == WM_CLOSE invoke SendMessage, hWnd, WM_COMMAND, IDM_sair, 0 .ELSEIF uMsg == WM_COMMAND ...
Gerenciando os botões Vimos acima que o parâmetro lParam identifica o remetente se a mensagem for de um controle. Caso contrário, o valor é NULL. Nossos botões são controles, então, as mensagens enviadas por eles terão lParam diferente de NULL. Acontece que precisamos do código de notificação dos botões para testar o evento (clique, duplo clique, etc) e este código se encontra no word mais significativo de wParam. Já vimos que, para obter o word menos significativo, basta transferir o valor de 32 bits para um registrador (EAX, por exemplo) e trabalhar com os primeiros 16 bits usando "meio" registrador (AX, por exemplo). Para acessar os 16 bits mais significativos precisamos usar um expediente: deslocar os bits da esquerda para a direita em 16 posições. A instrução que produz este deslocamento é a SHR (shift right - deslocamento para a direita). Usaremos o registrador EDX para efetuar o deslocamento e depois utilizaremos DX para obter o código de notificação (de 16 bits). Quando o botão com "Diga Oi" for acionado, queremos que o texto da caixa de edição seja preenchido com "Viu só? O gerente funcionou" que, claro, precisa ser declarado e inicializado na seção .DATA. Quando o botão "Sair" for acionado, aparece a caixa de mensagem com "Tchauzinho" e a janela de diálogo é fechada. dialog.asm
... .DATA NomeClasse db "DLGNumaBoa",0 TituloJanela db "O que o gerente fez",0 Adeus db "Tchauzinho",0 StringTeste db "Viu só? O gerente funcionou",0 ... .ELSEIF uMsg == WM_COMMAND mov eax, wParam .IF lParam == NULL .IF ax == IDM_pegar invoke GetDlgItemText, hWnd, IDC_EDIT, ADDR buffer, 512 invoke MessageBox, hWnd, ADDR buffer, ADDR TituloJanela, MB_OK .ELSEIF ax == IDM_pegar invoke SetDlgItemText, hWnd, IDC_EDIT, NULL
.ELSEIF ax == IDM_sair invoke MessageBox, hWnd, ADDR Adeus, ADDR TituloJanela, MB_OK invoke EndDialog, hWnd, NULL .ENDIF .ELSE mov edx, wParam shr edx, 16 .IF dx == BN_CLICKED // botão clicado? .IF ax == IDC_BUTTON invoke SetDlgItemText, hWnd, IDC_EDIT, ADDR StringTeste .ELSEIF ax == IDC_EXIT invoke SendMessage, hWnd, WM_COMMAND, IDM_sair, 0 .ENDIF .ENDIF .ENDIF ...
O valor de retorno do gerente de mensagens de uma janela de diálogo Você viu na rotina do gerente de mensagens de uma janela "normal", quando nenhuma mensagem é interceptada, é preciso redirigir a mensagem para o procedimento padrão do sistema com invoke DefWindowProc, hWnd, uMsg, wParam, lParam. Numa janela de diálogo, em princípio, é preciso fazer a mesma coisa - chamar o procedimento padrão. A diferença é a seguinte: não substituímos o gerente de diálogos, apenas o contratamos. Ele está "de serviço" e não precisamos chamá-lo através de uma função. É suficiente indicar para ele se nós já processamos a mensagem ou não, e isso é possível fazer com TRUE (verdadeiro) ou FALSE (false). Também sabemos que o registrador oficial para valores de retorno é o EAX. Então EAX = TRUE significa que processamos a mensagem; EAX = FALSE significa que a mensagem não foi processada e o gerente precisa tomar as devidas providências. dialog.asm
gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg == WM_INITDIALOG ... .ELSEIF uMsg == WM_CLOSE ... .ELSEIF uMsg == WM_COMMAND ... .ELSE mov eax, FALSE ret .ENDIF mov eax, TRUE ret
gerenteMensagem endp
Finalmentes Mais um tutorial do tamanho de um bonde! Mas é assim mesmo. Toda vez que se discute um assunto em detalhes, a conversa fica longa. Escreva e compile seu código. O resultado obtido deve ser este:
Como sempre, você pode fazer o download do tutorial ou visitar a seção de downloads / tutoriais / assembly numaboa para baixar a coleção completa. Abraços a todos da vó Vicki
Índice do Artigo Abre-te Sésamo (masm) Interceptando mensagens Todas as páginas Uma dialog box especial para abrir arquivos. As common dialog boxes são classes prontas para serem usadas. Veja como é fácil abrir arquivos e extrair as informações da seleção. Já vimos como fazer uma dialog box se comportar como janela principal (tutorial "Dialog Box"), agora é só explorar um pouco mais as possibilidades. Neste tutorial vamos ver como usar janelas de diálogo como dispositivos de entrada e saída - especificamente, como usar uma caixa de diálogo (dialog box), uma das muitas predefinidas do Windows, para abrir arquivos.
Um pouco de teoria
O Windows possui vários tipos de janelas de diálogo predefinidas, prontinhas para serem utilizadas nos seus aplicativos. São as chamadas common dialog boxes: caixas de diálogo para arquivos, impressão, cores, fontes e procura. As common dialog boxes fazem parte da comdlg32.dll de modo que, se quisermos fazer uso de alguma delas, precisamos incluir a biblioteca comdlg32.lib no nosso código fonte. Para criá-las, basta chamar a função correspondente: GetOpenFileName para abrir arquivos, GetSaveFileName para salvar arquivos, PrintDlg para imprimir e assim por diante. Neste tutorial vamos associar à janela principal do nosso aplicativo uma janela de diálogo para abrir arquivos. Como sempre, vamos partir do modelo habitual, aquele criado tutorial Janelas. Também precisamos criar um arquivo de recursos que contenha um menu com o item principal "Arquivo" com dois itens: "Abrir" e "Sair". A essa altura do campeonato, você tira isto de letra (ou então refresque um pouco a memória relendo "Criando menus em Assembly"). A principal novidade é só a função GetOpenFileName.
A função GetOpenFileName A função GetOpenFileName cria uma janela de diálogo comum do tipo Abrir (Open) que permite ao usuário especificar o drive, o diretório e o nome de um arquivo ou um conjunto de arquivos que devem ser abertos. função da API
BOOL GetOpenFileName( LPOPENFILENAME lpofn // endereço da estrutura com os dados de inicialização );
O único parâmetro desta função é lpofn, um ponteiro para uma estrutura OPENFILENAME que contém informações usadas para inicializar a janela de diálogo. No retorno da função, esta estrutura contém informações sobre o arquivo selecionado. Se o usuário especificar um nome de arquivo e clicar o botão OK, o valor de retorno da função será diferente de zero. Se o usuário cancelar ou fechar a janela de diálogo, ou se ocorrer algum erro, o valor de retorno será zero.
A estrutura OPENFILENAME A estrutura OPENFILENAME fornece todas as informações necessárias para inicializar uma janela de diálogo para abrir ou salvar arquivos. No retorno da função, a estrutura contém informações sobre a escolha efetuada pelo usuário. Ela possui 20 membros: typedef struct tagOFN { DWORD lStructSize;
// ofn O tamanho da estrutura em bytes
HWND hwndOwner;
O manipulador da janela à qual pertence a dialog
HINSTANCE hInstance;
O manipulador da instância do aplicativo que cria a
box
dialog box LPCTSTR lpstrFilter; Ponteiro para um buffer que contém pares de strings terminadas em zero. A última string precisa ser terminada com dois caracteres NULL. A primeira string de um par é descritiva, a segunda é um filtro. Por exemplo: "Bitmap" e "*.bmp". Se lpstrFilter for NULL, a janela de diálogo não mostra filtros. LPTSTR lpstrCustomFilter; Ponteiro para um buffer estático que guardará pares de string de filtros que o usuário indicar. Se for NULL, não retém os filtros indicados. DWORD nMaxCustFilter; O tamanho em bytes ou caracteres do buffer identificado por lpstrCustomFilter. O tamanho mínimo é 40. É ignorado se lpstrCustomFilter for NULL ou apontar para uma string NULL. DWORD nFilterIndex; Especifica qual par de strings filtro será usado na primeira vez em que a janela de diálogo for aberta. 1 para o primeiro par, 2 para o segundo, etc. Zero indica um filtro indicado pelo usuário. LPTSTR lpstrFile; Ponteiro para o buffer que contém o nome de arquivo usado para inicializar o controle de edição da dialog box. Após a seleção do usuário, guarda o nome do arquivo e seu caminho completo. DWORD nMaxFile; 256 bytes.
Tamanho do buffer lpstrFile. O tamanho mínimo é de
LPTSTR lpstrFileTitle; Ponteiro para um buffer que recebe o nome do arquivo selecionado e sua extensão (sem caminho). Este membro pode ser NULL. DWORD nMaxFileTitle; Tamanho do buffer para lpstrFileTitle. É ignorado se lpstrFileTitle for NULL. LPCTSTR lpstrInitialDir; Ponteiro para uma string que especifica o diretório inicial. Se for NULL, o sistema usa o diretório atual. LPCTSTR lpstrTitle; diálogo.
Ponteiro para a string com o título da janela de
DWORD Flags; Um jogo de bits sinalizadores. Pode ser a combinação de vários sinalizadores. OFN_ALLOWMULTISELECT, OFN_FILEMUSTEXIST e OFN_PATHMUSTEXIST são alguns exemplos. WORD nFileOffset; Após a seleção de um arquivo, este membro contém o índice do primeiro caracter do nome do arquivo. Por exemplo, se o selecionado foi "c:\dir1\dir2\teste.exe", este membro conterá 13 que indica a posição do "t". WORD nFileExtension; Após a seleção, contém o índice do primeiro caracter da posição. No mesmo exemplo usado acima, seu valor será 18 e apontará para "." LPCTSTR lpstrDefExt; Ponteiro para um buffer que contém a extensão default. Se o usuário não indicar a extensão, esta será automaticamente adicionada quando um arquivo for salvo.
DWORD lCustData; Dados definidos no aplicativo que o sistema passa para o procedimento hook identificado pelo membro lpfnHook. LPOFNHOOKPROC lpfnHook; Ponteiro para o procedimento hook (gancho). Por exemplo, você pode dar o aspecto do Explorer à sua janela de diálogo. Para obter este efeito, há a necessidade de um procedimento "enganchado". LPCTSTR lpTemplateName; Ponteiro para uma string terminada em zero com o nome de um modelo de diálogo existente no arquivo de recursos. Este membro é ignorado se o sinalizador OFN_ENABLETEMPLATE, do membro Flags, não estiver ativado. } OPENFILENAME;
Apesar desta estrutura ser muito extensa, ainda assim você terá muito menos trabalho criando e inicializando esta estrutura do que fazendo uma janela de diálogo completa para a abertura de arquivos. Podes crer.
O "Abre-te Sésamo" Como foi dito acima, o programa exemplo mostrará uma janela de diálogo para abrir um arquivo quando o usuário selecionar o item de menu Arquivo - Abrir. Após selecionar um arquivo, uma caixa de diálogo mostrará o nome do arquivo com o caminho completo, o nome do arquivo sem o caminho e a extensão do arquivo. Então vamos lá. A primeira providência é preparar o menu.
O menuzinho O arquivo de recursos
// Constantes para o menu #define IDM_abrir 1 #define IDM_sair 2
Menuzinho MENU { POPUP "&Arquivo" { MENUITEM "&Abrir",IDM_abrir MENUITEM SEPARATOR MENUITEM "&Sair",IDM_sair } }
Digite o texto acima, salve como RSRC.RC e compile. Agora vamos incluí-lo no no código: openFile.asm
... .CONST
IDM_abrir equ 1 IDM_sair equ 2 ... .DATA NomeClasse db "Janela", 0 TituloJanela db "Abre-te Sésamo", 0 NomeMenu db "Menuzinho", 0 .CODE entrada: ... gerenteJanela PROC ... ... mov ej.lpszMenuName, NomeMenu ...
Interceptando a mensagem do menu Já sabemos que mensagens de menu são do tipo WM_COMMAND e que podem ser interceptadas pelo gerenteMensagem: openFile.asm
... gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ... .ELSEIF uMsg == WM_COMMAND mov eax, wParam .IF ax == IDM_abrir ... // Aqui entra o código da Open Dialog Box .ELSE ...
Criando a estrutura OPENFILENAME Se o item de menu "Abrir" for selecionado, precisamos ter à disposição uma estrutura OPENFILENAME declarada para poder inicializá-la. Só depois disso é que poderemos criar a Open Dialog Box. Analisando os membros da estrutura, fica evidente que vamos precisar de uma string de filtro, um buffer para guardar o arquivo selecionado e do tamanho do buffer. openFile.asm
.CONST ... TamBuffer equ 256 .DATA
... ofn OPENFILENAME <> StringFiltro db "Todos (*.*)", 0, "*.*", 0 db "Arquivos Texto (*.txt)", 0, "*.txt", 0, 0 Buffer db TamBuff dup (0) .CODE ... gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ... .ELSEIF uMsg == WM_COMMAND mov eax, wParam .IF ax == IDM_abrir mov ofn.lStructureSize, SIZEOF ofn push hWnd pop ofn.hWndOwner push mInstancia pop ofn.hInstance mov ofn.lpstrFilter, OFFSET StringFiltro mov ofn.lpstrFile, OFFSET Buffer mov ofn.nMaxFile, TamBuff mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY ... .ELSE ... .ENDIF .ELSE // usuário escolheu item de menu "Sair" INVOKE DestroyWindow, hWnd .ENDIF ...
Analisando a seleção do usuário Tendo inicializado a estrutura OPENFILENAME, agora é possível criar a janela de diálogo padrão para a abertura de arquivos. Assim que o usuário fizer a sua escolha, analisaremos os valores de retorno e os apresentaremos numa caixa de mensagens. Novamente precisaremos de alguns dados adicionais: um buffer para as strings que serão apresentadas, o tamanho deste buffer, os textos padrão para o nome do arquivo selecionado e o título da caixa de mensagem. A concatenação das strings padrão com as strings da seleção pode ser obtida através da função lstrcat: função da API
LPTSTR lstrcat( LPTSTR lpString1, // endereço do buffer para as strings concatenadas LPCTSTR lpString2 // endereço da string que deve ser adicionada à string1 ); openFile.asm
.CONST
... TamBuffer TamResp
equ 256 equ 512
.DATA ... ofn StringFiltro
OPENFILENAME <> db "Todos (*.*)", 0, "*.*", 0 db "Arquivos Texto (*.txt)", 0, "*.txt", 0, 0 Buffer db TamBuff dup (0) BuffResp db TamResp dup (0) NomeCompleto db "O nome completo do arquivo é: ", 0 NomeArq db "O nome do arquivo é: ", 0 NomeExt db "A extensão do arquivo é: ", 0 QuebraDeLinha db 0Dh, 0Ah, 0 TituloResp db "Você selecionou", 0
... gerenteMensagem proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM ... .ELSEIF uMsg == WM_COMMAND ... .IF ax == IDM_abrir ... INVOKE GetOpenFileName, ADDR ofn .IF eax == TRUE INVOKE lstrcat, OFFSET BuffResp, OFFSET NomeCompleto ; põe "O nome completo do arquivo é: " no buffer INVOKE lstrcat, OFFSET BuffResp, ofn.lpstrFile ; concatena com o ; nome e caminho do arquivo escolhido INVOKE lstrcat, OFFSET BuffResp, OFFSET QuebraDeLinha ; concatena com uma quebra de linha INVOKE lstrcat, OFFSET BuffResp, OFFSET NomeArq ; concatena com "O nome do arquivo é: " mov eax, ofn.lpstrFile ; põe o ponteiro para o nome com caminho ; no registrador eax push ebx ; põe valor de ebx na pilha, preservando-o xor ebx, ebx ; zera ebx mov bx, ofn.nFileOffset ; põe a posição do ; primeiro caracter do nome do arquivo ; no registrador ebx add eax, ebx ; avança o ponteiro para apontar o início do ; nome do arquivo pop ebx ; restaura o valor original de ebx INVOKE lstrcat, OFFSET BuffResp, eax ; concatena com o nome do arquivo INVOKE lstrcat, OFFSET BuffResp, OFFSET QuebraDeLinha
; concatena mais uma quebra de linha INVOKE lstrcat, OFFSET BuffResp, OFFSET NomeExt ; concatena "A extensão do arquivo é: " mov eax, ofn.lpstrFile ; ponteiro para o nome com caminho em eax ; preserva o valor de ebx push ebx xor ebx, ebx ; zera ebx mov bx, ofn.nFileExtension ; põe a posição do primeiro caracter da extensão ; do arquivo no registrador ebx add eax, ebx ; avança o ponteiro para apontar o início ; da extensão do arquivo ; restaura o valor original de ebx pop ebx INVOKE lstrcat, OFFSET BuffResp, eax ; concatena com a extensão do arquivo INVOKE MessageBox, hWnd, OFFSET BuffResp, ADDR TituloResp, MB_OK ; mostra o resultado INVOKE RtlZeroMemory, OFFSET BuffResp, TamResp ; limpa o buffer de strings de resposta .ENDIF .ELSE ...
Finalmentes O pequeno aplicativo tem este jeitão:
Você pode fazer o download do tutorial nos Downloads da Aldeia em Tutoriais / Assembly Numaboa.
Este texto, baseado no original de Jeremy Gordon, autor dos programas GoAsm, GoRC, GoLink e GoBug, parte do princípio que você seja realmente um iniciante em programação e não sabe bulhufas sobre registradores e blocos de elementos funcionais. No começo é assim mesmo e não tem a mínima importância. Vamos lá!
O que é um programa Um programa contém instruções para o processador do computador. As instruções são chamadas de código do programa. O processador executa as instruções do código, uma por uma – e é por isso mesmo que o programa é chamado de executável. Quando um programa é carregado, o Windows dá ao processador o endereço inicialpresente no código, onde a execução deve começar. A partir deste ponto fica a cargo do programa desviar a execução para os pontos apropriados do seu código. Falaremos deste assunto importante mais tarde.
O que faz um programa Na verdade, o que faz o código? Uma das coisas é transferir números para dentro e para fora dos registradores que existem dentro do processador. Existem oito registradores de uso geral. Cada registrador guarda 32 bits de dados. Isto é denominado de dword (literalmente, palavra dupla ou "double word"). Num dado momento, cada um dos bits pode estar ligado ou desligado (um ou zero, setado ou zerado, etc). Isto é um número binário. Em 32 bits, se todos os bits estiverem ligados, o número decimal correspondente é 4.294.967.295. Também existem diversas instruções aritméticas que podem ser executadas pelo processador. Outra coisa que o código faz é transferir números dos registradores para a memória e vice-versa, ou colocar números na pilha da memória. A pilha é como se fosse uma pilha de pratos. Você põe um dword na pilha usando a instrução PUSH e retira o último dword que foi PUSHado para o topo da pilha usando a instrução POP. A memória à qual me refiro é a memória RAM (random access memory - memória de acesso randômico), que fica dentro do computador. A pilha fica reservada quando o programa é carregado. Dependendo dos seus dados de instrução, outra área de memória é reservada pelo sistema quando o programa é carregado. Esta memória é chamada de dados do programa. Mais memória será requisitada pelo programa em tempo de execução.
O contexto Windows No contexto Windows, uma das coisas mais importantes que as instruções do código fazem é chamar funções do Windows , ou seja, desviar a execução para estas funções. As funções do Windows são chamadas de API(Application Programmer’s Interface). Estas funções são armazenadas em DLLs do Windows . Uma Dll é um arquivo executável fornecido pelo sistema operacional Windows. Geralmente estão numa das pastas (diretórios) onde o Windows foi instalado. A maioria das instruções de código restantes são usadas para preparar estas chamadas e para lidar com o resultado delas. São estas chamadas às APIs que dão uma funcionalidade extraordinária aos seus programas. A possibilidade de chamar o Windows oferece uma imensa gama de processos que resultarão em interação com o usuário, apresentação adequada na tela, impressão, ações com arquivos, etc. Todas estas ações, na realidade, são realizadas pelo Windows assim que forem requisitadas pelo programa.
O processo de construção Para construir um executável, você vai precisar de ferramentas de desenvolvimento. Como disse anteriormente, este texto se baseia num artigo de Jeremy Gordon. Portanto, as ferramentas usadas serão softwares do autor, disponíveis gratuitamente no seu site, Win32 + Assembler Source Page:
GoAsm - o assembler. Este programa transforma seu código fonte num arquivo objeto. Para escrever seu código fonte em texto puro você pode utilizar qualquer editor de texto, por exemplo, o Bloco de Notas ou o Wordpad. Assegure-se apenas de salvar o script em texto sem caracteres de controle especiais. Normalmente a extensão do arquivo deve ser ".asm". O script deve conter as linhas de código fonte, o qual contém o código e os dados de instrução. O arquivo objeto é um arquivo preparado para que possa ser lido pelo linker, que é usado para produzir o executável final. GoRC - o compilador de recursos. Este programa converte seu script de recursos num arquivo res. O script de recursos é outro arquivo de texto puro só que, neste caso, contém instruções para criar controles do Windows para o seu programa menus, diálogos, ícones, bitmaps e tabelas de strings. Normalmente possui a extensão rc. Os arquivos res também precisam ser preparados para que possam ser lidos pelo linker. GoLink - o linker. Este programa utiliza um ou mais arquivos objeto e arquivos res para criar o executável final. GoBug - o debugger. Com esta ferramenta você pode observar seu programa enquanto é executado. Cada instrução, uma a uma, pode ser rastreada e você pode ver como elas afetam os registradores e as áreas de memória.
O processo de construção pode ser resumido da seguinte forma:
Código Fonte Linker texto puro .asm -> .obj .rc -> .res
Arquivo Executável -> .exe
Um programa que não faz nada Faremos um programa que carrega, executa e termina sem fazer qualquer outra coisa. Isto é apenas para demonstrar o processo de construção e para explicar mais algumas coisinhas. Crie um diretório (ou pasta) com o nome e a localização que mais lhe convier, por exemplo C:\GoAsm\. Isto ajuda na organização do seu trabalho. Você pode colocar todos seus arquivos e ferramentas neste diretório ou em seus sub-diretórios, como preferir. Copie GoAsm, GoRC, GoLink (e GoBug) neste diretório.
Usando seu editor de texto, crie um novo arquivo e digite as linhas seguintes: nada.asm
1. CODE SECTION 2. START: 3. RET
Salve este arquivo no diretório de trabalho com o nome, digamos, de nada.asm. Este é o programa "faz nada".
CODE SECTION diz ao assembler que, o que se segue, são códigos de instrução. START é um marcador (ou etiqueta) de início do código. Este marcador indica ao linker que este é o ponto onde deve começar a execução do programa. RET é um mnemônico (instrução do processador em palavras) que diz ao processador para RETornar ao código chamador que, neste caso, é o próprio Windows.
Usando novamente seu editor de texto, crie um novo arquivo e digite o seguinte: gonada.bat
1. 2.
GoAsm nada GoLink nada.obj /console
Salve este arquivo no diretório de trabalho com o nome de, por exemplo, gonada.bat. Você acaba de criar um arquivo de lote (batch file), que é um pequeno arquivo muito útil que executa uma linha após a outra através da janela MSDOS. A primeira linha executa o assembler GoAsm que vai usar o arquivo nada.asm (assumindo que a extensão seja asm). Isto cria o arquivo nada.obj, que então é utilizado pelo GoLink para criar o executável nada.exe. GoLink também recebe a informação para criar um programa de console - este é um programa que não faz uso da Interface Gráfica do Windows (o programa não tem janelas) Dê um duplo clique sobre gonada.bat e observe o assembler e o linker fazerem seu trabalho. Se preferir, abra a janela do MSDOS, vá ao diretório de trabalho e digite gonada.bat - o efeito é o mesmo. Dê um duplo clique sobre nada.exe e observe. Se nada acontecer, parabéns! Você acaba de construir seu primeiro programa em Assembly e, o mais importante, sem erros! Se você receber alguma mensagem de erro ou se o executável não foi gerado, revise os textos tanto do código fonte quanto do batch file, verifique se todos os arquivos necessários (fontes, assembler e linker) estão no mesmo diretório e repita o processo.
Um programa que escreve "NumaBoa" Agora faremos um programa que é carregado através do MSDOS e que escreve "NumaBoa" na tela. É apenas para demonstrar como chamar uma API do Windows. Este tipo de programa é chamado de programa de console, por que ele não faz janelas, ou seja, não faz uso da GUI (Graphical User Interface) do Windows. O processo é o mesmo que o anterior. Apenas é preciso mudar os nomes do código fonte, por exemplo para numaboa.asm, e do arquivo de lote, por exemplo para gonumaboa.bat. Digamos que você use o seguinte para o batch file: gonumaboa.bat
1. GoAsm numaboa 2. Golink /console /debug coff numaboa.obj Kernel32.dll
Na primeira linha, GoAsm numaboa vai transformar numaboa.asm em numaboa.obj. A segunda linha tem algumas novidades. Em primeiro lugar, "/debug coff". Isto adiciona informações de debug ao executável de modo que os símbolos que você esteja usando aparecerão num debugger (usando um debugger você pode executar o programa linha por linha). Isto é opcional e você deveria usar esta chave do linker apenas enquanto estiver desenvolvendo seu programa e tiver que efetuar testes. "Kernel32.dll" é uma biblioteca necessária para informar o linker de que deve obter as funções da API requeridas pelo programa no arquivo Kernel32.dll do Windows. No caso serão utilizadas as funções GetStdHandle e WriteFile. Crie um novo arquivo e digite as seguintes linhas: numaboa.asm
1. DATA SECTION 2. WRKEEP DD 0
DATA SECTION diz ao assembler que, a seguir, vem uma instrução (ou instruções) de dados. WRKEEP é um marcador de dados cujo nome indica um lugar em particular na área de dados. DD cria um dword (4 bytes) na área de dados (literalmente "declara dword"). 0 inicializa o dword com um valor, neste caso zero. Seguindo as linhas acima digite o seguinte: numaboa.asm
3. CODE SECTION 4. START: 5. PUSH -11
6. CALL GetStdHandle
CODE SECTION e START você já conhece. PUSH põe um valor na pilha. O valor, neste caso, é menos 11 (expresso como um número decimal normal). Isto é PUSHado para a pilha, pronto para ser enviado como parâmetro na chamada da API. CALL transfere a execução para um procedimento, neste caso a função GetStHandle da API do Windows que se encontra na Kernel32.dll. Esta função precisa de um parâmetro e devolve um manipulador (handle) no registrador EAX. Aqui, solicitamos um manipulador -11, que é o manipulador padrão de saída (output) para o console. O console é a interface do usuário que, neste caso, será o MSDOS do Windows. Através deste manipulador poderemos escrever na tela da janela MSDOS. A seguir adicione: numaboa.asm
7. PUSH 8. PUSH 9. PUSH 10. CALL
0, ADDR WRKEEP 9, 'NumaBoa' EAX
WriteFile
Aqui PUSHamos parâmetros que serão utilizados na chamada à função da API WriteFile. Esta função escreve no manipulador passado como último parâmetro (que ainda se encontra no registrador EAX). Ela escreve a string de 9 caracteres de comprimento. Também fornecemos o endereço de WRKEEP, o dword de dados feito anteriormente. Isto é necessário porque WriteFile colocará neste dword quantos caracteres foram escritos. Agora os finalmentes: numaboa.asm
11. MOV EAX,0 12. RET
Aqui colocamos o valor 0 no registrador EAX (para indicar um final com sucesso) e usamos o RET para voltar ao sistema (fechando o programa). Compile e linke o programa dando um duplo clique em gonumaboa.bat. Para observar o programa escrever "NumaBoa" na tela você precisa executá-lo através da janela MSDOS, uma vez que se trata de um programa de console.
Agradecimento Um agradecimento especial a Jeremy Gordon, autor do software e do artigo que serviu de base para este texto. O site do autor é em inglês e, sem dúvida alguma, vale uma visita. Seu trabalho é sério e de excelente qualidade, contribuindo sobremaneira para o
aprimoramento do Assembly e sendo um exemplo de despreendimento ao disponibilizar gratuitamente seu software para a comunidade.
Índice do Artigo Criando uma mini-calculadora em Assembly para Win32 Tratando as mensagens Funcionalidade dos botões Código fonte completo Todas as páginas Bem, pessoal, este é o meu primeiro tutorial sobre alguma coisa, então, se for mal escrito ou mal explicado, já peço desculpas. Há um tempo atrás já tinha lido sobre assembly, mas não consegui aprender nada, aí dei uma lida em um tutorial da Vó Vicki, sobre como criar janelas em ASM e desenvolvi uma MINI-Calculadora em Assembly para Win32. Pode ter alguns erros ou falta programar algumas coisas, mas funciona e serve muito pra você aprimorar seus conhecimentos e muito mais.
Conhecimento preciso Você precisa saber pelo menos o básico sobre como criar janelas em ASM, registrá-las e mostrá-las na tela. Se você tiver alguma dúvida sobre isto, veja o tutorial sobre como criar sua primeira janela. Clique no link abaixo: Link para o tutorial sobre Janelas Pronto! Você precisa deste tutorial para saber o básico sobre Janelas. O programa que foi usado para fazer o desenvolvimento do aplicativo foi o Masm32, então acredito que você vai precisar dele também ou algum outro de sua preferência. Ah, e com certeza você também precisa de conhecimento em Assembly, não muito, mas você precisa.
Começando Eu sempre quis fazer primeiros algumas coisas para depois aprender como elas funcionam, mas não façam isso. Se você não souber nada ou pouca coisa sobre como criar janelas, volte um pouco e
leia o tutorial indicado. Bem, então vamos ver o que vai ser feito para ficarmos logo por dentro de tudo! Essa aí seria a nossa calculadora, mas é claro que, depois de terminada, você pode implementar algumas coisas a mais como um menu com uma About Box, mas isto será feito em outra ocasião por você mesmo. Vamos primeiro tentar desenvolver esta calculadora básica. Primeiro começamos com o cabeçalho do programa e, nesta calculadora, você não precisa mais do que instruções do processador .386. Então você já sabe o que fazer, né? Aqui começa o código começa: .386 .MODEL Flat, StdCall Option casemap:none
Logo no começo iremos precisar apenas das bibliotecas user32.lib e Kernel32.lib. Então, continuando com o código: include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
Agora vamos inserir o protótipo Gerenciador_Janela: Gerenciador_Janela proto :DWORD, :DWORD, :DWORD, :DWORD
Você pode chamar esse protótipo da forma que quiser, desde que você saiba que ele servirá de modelo para o procedimento que criaremos mais à frente para gerenciar a janela Como sabemos, a seguir vêm as variáveis inicializadas. No momento precisamos apenas das principais para criarmos a janela onde ficarão os botões. Indicamos o título da janela e o nome da classe da janela: DATA Titulo_Janela db "Calculadora em ASM", 0 Classe_Janela db "Form1",0
A seguir vem o Handle de novas janelas e a Linha de Comando da janela (a linha de comando é opcional, mas, por via das dúvidas, vamos colocá – la), e depois os Handles dos botões e do edit: .DATA? Handle_Janela DWORD ? LinhaComando DWORD ?
Agora vem o código propriamente dito: .CODE Inicio: Invoke GetModuleHandle, NULL Mov Handle_Janela, eax Invoke GetCommandLine Mov LinhaComando, eax Invoke Gerenciador_Janela, Handle_Janela, NULL, LinhaComando, SW_SHOWDEFAULT Invoke ExitProcess, 0
Programa começado, agora precisaremos escrever o procedimento que gerenciará nossa janela, criando-a e registrando-a. Gerenciador_Janela proc hInstance :DWORD, hInstAntiga :DWORD, LnComando :DWORD, Tipo_Janela: DWORD
Nomearei a classe da minha janela de "wnd", abreviação de Window. LOCAL wnd: WNDCLASSEX LOCAL Janela: HWND LOCAL Mensagem: MSG ;;Criando a janela e registrando
mov wnd.cbSize, SIZEOF WNDCLASSEX mov wnd.style, CS_HREDRAW or CS_VREDRAW mov wnd.lpfnWndProc, offset GerenteMensagem mov wnd.cbClsExtra, NULL mov wnd.cbWndExtra, NULL push Handle_Janela pop wnd.hInstance invoke LoadIcon, NULL, IDI_WINLOGO mov wnd.hIcon, eax mov wnd.hIconSm, eax invoke LoadCursor, NULL, IDC_ARROW mov wnd.hCursor, eax mov wnd.hbrBackground, COLOR_BTNFACE+1 mov wnd.lpszMenuName, NULL mov wnd.lpszClassName, OFFSET Classe_Janela invoke RegisterClassEx, ADDR wnd
Bem, no tutorial de janelas você aprendeu a chamar a API do Windows (CreateWindowEx). Espero que você tenha entendido ela bem, por que nós a usaremos para criar os botões e os edits. invoke CreateWindowEx, NULL, ADDR Classe_Janela, ADDR Titulo_Janela,WS_OVERLAPPEDWINDOW, 433, 302, 230,190, NULL, NULL, hInstance, NULL
Você talvez deva ter achado aqueles números na chamada estranhos, mas, para refrescar a memória, eles significam:
433: Distância do começo de sua janela, que é o seu lado esquerdo, até o início esquerdo da tela do seu computador. 302: Distância do topo de sua janela até o topo da tela do seu computador. 230: Dimensão da largura da sua janela. 190: Dimensão da Altura da sua janela.
Você pode defini-los como quiser. mov Janela, eax invoke ShowWindow, Janela, SW_SHOWNORMAL invoke UpdateWindow, Janela .WHILE TRUE invoke GetMessage, ADDR Mensagem, NULL, 0,0 .BREAK .IF (eax < 1) invoke TranslateMessage, ADDR Mensagem invoke DispatchMessage, ADDR Mensagem .ENDW mov eax, Mensagem.wParam ret Gerenciador_Janela endp
Agora que já temos o nosso procedimento de criar a janela feito iremos para onde trataremos as mensagens, onde saberemos qual foi a mensagem recebida e o que será feito. É lá onde o negócio legal vai começar! Continuando com o código: GerenteMensagem proc hWnd: DWORD,
uMsg: UINT, wParam: WPARAM, lParam: LPARAM
Primeiro verificamos se a mensagem que foi recebida é WM_DESTROY. Se for, feche o programa, caso contrário prossiga com as nossas instruções. .If uMsg == WM_DESTROY Invoke PostQuitMessage, NULL
Bem, aí sabemos que o programa recebeu a mensagem para ser fechado e depois disto nada mais pode ser feito. Agora chegou a hora em que temos que criar o Edit Principal da Calculadora, onde ficarão os números, e depois os botões. Para criá-los, verificamos se a mensagem é igual a WM_CREATE; se for, podemos criar os edits e os botões.
.ELSEIF uMsg == WM_CREATE
Bem, pessoal, como meu irmão sempre me disse, tudo no Windows é uma janela. Pode não ser tudo, mas pelo menos quase tudo é: botões, edits, ListBox, etc, são todos janelas, porém são diferentes da nossa janela "mãe". São janelas filhas e, se são filhas, com certeza precisam de uma janela mãe. Então, já que agora sabemos que são janelas filhas e você já tem a mãe delas, então é só chamar a API CreateWindowEx pra criar novas janelas. Mas como sabemos, uma janela precisa basicamente de uma classe, de um título e de um Handle. Então sabemos agora que precisamos criar variáveis novas para o nosso programa. Para criar o Edit, não precisamos necessariamente de um título então crie somente o nome da classe do Edit e uma variável não inicializada para ficar com o Handle do Edit. Voltando ao início do código você teria: .DATA Titulo_Janela db "Calculadora em ASM", 0 Classe_Janela db "Form1",0 Edit_Classe db "Edit", 0 .DATA ? Handle_Janela DWORD ? LinhaComando DWORD ? EditHandle DWORD ?
Pronto. Agora podemos criar nosso novo Edit! É só chamar a API CreateWindowEx Invoke CreateWindowEx, NULL, ADDR Edit_Classe, NULL, WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or ES_AUTOHSCROLL, 8, 8, 193, 21, hWnd, NULL, Handle_Janela, NULL Mov EditHandle, eax .ELSE Invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret .ENDIF GerenteMensagem endp End Inicio
A partir de agora temos o nosso edit criado. Se você quiser executá-lo neste ponto, se o seu código estiver correto você já terá um edit na tela.
Criando os botões Agora iremos aprender ou, se já sabemos, iremos criar os botões na nossa tela. Os botões irão de 0 até 9 e ainda teremos os botões de Soma, Subtração, Multiplicação e Divisão. Um ainda não implementado por mim, mas que pode ser implementado por
vocês, é o da vírgula ou ponto como queira. Teremos ainda o CE, e por fim o Botão C. Se já sabemos quais botões temos que criar, então vamos criar a classe de todos os botões. Voltando novamente ao início: .DATA ... ; Logo após as variáveis que você já criou, crie uma agora que conterá a classe do botão Botao_Classe db "Button", 0 ; Classe criada, mas nós queremos mostrar o titulo do botão nele para sabermos quem é o ; Botão 1, o 2 , 3 e etc... sendo assim, temos que criar o titulo desse botão. Botao1_Titulo db "1",0 .DATA? ... ; Logo após todas as variáveis que você já criou dentro dessa seção, você terá que criar outra. ; Sabemos que um botão é uma janela, então significa que ele terá que ter seu próprio Handle. ; Então temos que criar um novo Handle para o botão que vai ter o número 1. Botao1_Handle DWORD ?
Agora podemos tranquilamente criar o nosso primeiro botão, que será o botão que conterá o número 1. Teremos que ir agora pra o procedimento GerenteMensagem e, logo abaixo de onde criamos o nosso edit, vamos criar o resto dos botões. Invoke CreateWindowEx, NULL, ADDR Edit_Classe, NULL, WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or ES_AUTOHSCROLL, 8, 8, 193, 21, hWnd, NULL, Handle_Janela, NULL Mov EditHandle, eax
Logo após essa parte do código chamaremos novamente a função CreateWindowEx para criar o primeiro botão, mas, antes disso, um problema que talvez seja achado por muitos "difícil" de resolver é onde ficará a posição dos botões na tela definida através de valores. Com o tempo, porém, se você pegar o jeito, isso deixa de ser um "problema" e sem contar que você pode usar suas técnicas Mas não se preocupe, no tutorial os botões já virão todos com suas posições definidas na tela, e lembrando que você pode alterar a posição deles na hora que quiser. Então vem o código do botão. Invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao1_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 8, 32, 33, 25, hWnd, NULL, Handle_Janela, NULL Mov Botao1_Handle, eax
Se você adicionar o código e executar o programa, terá um botão na tela. Criar os outros botões não é diferente. Você só tem que se lembrar de criar uma nova variável em .DATA com o título do botão e uma nova em .DATA? que salvará o Handle do novo botão. Podemos fazer aquele velho CTRL + C e CTRL + V, sem esquecer de alterar algumas coisas.
OBS: A variável Botão_Classe pode ser usada para criar o resto de todos os outros botões. Então ela não muda, por que todos pertencem à mesma classe - a classe não precisa ser recriada. Ah, se você quiser pegar mais familiaridade com a API CreateWindowEx, então crie todos os botões digitando os códigos, sem copiar e colar. Segue abaixo o código para criar o resto dos botões com as devidas posições. Primeiro adicionar os títulos de todos os botões e os Handles para os mesmos na seção .DATA e .DATA? .DATA ... Botao2_Titulo db "2", 0 Botao3_Titulo db "3", 0 Botao4_Titulo db "4", 0 Botao5_Titulo db "5", 0 Botao6_Titulo db "6", 0 Botao7_Titulo db "7", 0 Botao8_Titulo db "8", 0 Botao9_Titulo db "9", 0 Botao0_Titulo db "0", 0 BotaoVirgulaTitulo db ",", 0 BotaoIgualTitulo db "=", 0 BotaoMaisTitulo db "+", 0 BotaoMenosTitulo db "-", 0 BotaoMultiplicarTitulo db "*", 0 BotaoDividirTitulo db "/", 0 BotaoCETitulo db "CE", 0 BotaoCTitulo db "C", 0 .DATA? ... Botao2Handle DWORD ? Botao3Handle DWORD ? Botao4Handle DWORD ? Botao5Handle DWORD ? Botao6Handle DWORD ? Botao7Handle DWORD ? Botao8Handle DWORD ? Botao9Handle DWORD ? Botao0Handle DWORD ? BotaoVirgulaHandle DWORD ? BotaoMaisHandle DWORD ? BotaoMenosHandle DWORD ? BotaoMultiplicarHandle DWORD ? BotaoDividirHandle DWORD ? BotaoIgualHandle DWORD ? BotaoCEHandle DWORD ? BotaoCHandle DWORD ?
Logo após o código do botão 1 você pode implementar o código de acordo com meu... invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao2_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 48, 32, 33, 25, hWnd, NULL, Handle_Janela, NULL
mov Botao2Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao3_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 88, 32, 33, 25, hWnd, NULL, Handle_Janela, NULL mov Botao3Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao4_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 8, 64, 33, 25, hWnd, NULL, Handle_Janela, NULL mov Botao4Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao5_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 48, 64, 33, 25, hWnd, NULL, Handle_Janela, NULL mov Botao5Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao6_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 88, 64, 33, 25, hWnd, NULL, Handle_Janela, NULL mov Botao6Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao7_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 8, 96, 33, 25, hWnd, NULL, Handle_Janela, NULL mov Botao7Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao8_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 48, 96, 33, 25, hWnd, NULL, Handle_Janela, NULL mov Botao8Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao9_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 88, 96, 33, 25, hWnd, NULL, Handle_Janela, NULL mov Botao9Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao0_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 8, 128, 33, 25, hWnd, NULL, Handle_Janela, NULL mov Botao0Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao0_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 8, 128, 33, 25, hWnd, NULL, Handle_Janela, NULL mov Botao0Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoVirgulaTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 48, 128, 33, 25, hWnd, NULL, Handle_Janela, NULL mov BotaoVirgulaHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoIgualTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 88, 128, 33, 25, hWnd, NULL, Handle_Janela, NULL
mov BotaoIgualHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoMaisTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 128, 32, 33, 25, hWnd, NULL, Handle_Janela, NULL mov BotaoMaisHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoMenosTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 128, 64, 33, 25, hWnd, NULL, Handle_Janela, NULL mov BotaoMenosHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoMultiplicarTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 128, 96, 33, 25, hWnd, NULL, Handle_Janela, NULL mov BotaoMultiplicarHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoDividirTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 128, 128, 33, 25, hWnd, NULL, Handle_Janela, NULL mov BotaoDividirHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoCETitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 168, 32, 33, 25, hWnd, NULL, Handle_Janela, NULL mov BotaoCEHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoCTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 168, 64, 33, 25, hWnd, NULL, Handle_Janela, NULL mov BotaoCHandle, eax
Pronto. Com todas essas adições nós temos o design final da nossa mini_calc.
Dando funcionalidade aos botões Agora vem a parte onde você tem que programar o que o botão vai fazer ao ser clicado pelo usuário. É só verificar se a mensagem agora é um WM_COMMAND. Se for, teremos que fazer outra verificação, teremos que pegar o valor retornado na mensagem no parâmetro do procedimento que nós chamamos anteriormente de lParam. Certo? Vamos ver como fazer isso na prática. Logo abaixo do código do último botão (o botão "C"), vamos ter que implementar mais código, agora pra dizermos o que os botões irão fazer. ... .ELSEIF uMsg == WM_COMMAND ;;Movendo o valor de lParam para edx Mov edx, lParam ;;Comparando edx com o Handle do Botão 1
.if edx == Botao1_Handle
Bem, nesse momento você deve ter pensado que agora era só colocar uma chamada para SetWindowText passando o valor do título para o botão. Se você pensou assim, você não errou. Porém há algo mais a se pensar: se o usuário clicar no botão 1, logicamente o texto do nosso edit seria 1, certo? Mas se o usuário clicasse no 2, então o texto do edit seria 2 e não 12. É ai onde entra a função lstrcat. Breve explicação sobre a função lstrcat: lstrcat recebe duas strings terminadas em 0 e retorna um ponteiro que nos indica onde está a junção das duas strings que foram passadas, e esse ponteiro é retornado no registrador eax. Já que sabemos isso, vamos programar; só precisamos da lógica. Primeiro pegaremos o texto que está no Edit. Então vamos lá! Vamos criar uma variável para guardar o texto do edit lá em data?. .DATA? ... Edit_Texto db 100 dup (?)
Indo agora para o código do Botao1, chamaremos a função GetWindowText para pegar o texto do Edit: ... Invoke GetWindowText, Edit_Handle, ADDR Edit_Texto, 100
E agora chamamos lstrcat passando as duas strings. Invoke lstrcat, ADDR Edit_Texto, ADDR Botao1_Titulo
E você terá que guardar o valor do ponteiro retornado em alguma variável. Então declare uma em Data? do tipo DWORD. .DATA ... Endereco_String DWORD ?
Agora que já temos a variável é só mover o valor para dentro dela. Mov Endereço_String, eax
Agora, como você tinha pensado (ou não), chamaremos a API SetWindowText. Invoke SetWindowText, Edit_Handle, [Endereco_String] .endif
Pronto, terminamos o código do botao1. Pense e faça a mesma coisa para os outros botões, menos virgula, adição, subtração e tal, tal, tal...
Aqui vai o código completo de mais dois botões. Faça o mesmo para os botões restantes: ... .if edx == Botao2Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao2_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] .endif .if edx == Botao3Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao3_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] .endif
Tendo todos os botões codificados, agora só precisamos configurar os botões das operações. Iremos começar pelo botão de Adição, os outros três são semelhantes mudando poucas funções. .if edx == BotaoMaisHandle
Agora vamos ter que fazer algo bem legal - conversões. Você não pode somar duas strings, então você tem que converte-la num valor inteiro e salvá-lo em algum lugar. Então vamos pensar um pouco: quando o usuário digitar alguns números e depois clicar em mais, você terá que salvar o valor digitado e já convertido e limpar o edit para a entrada do outro valor. Isto nos diz que precisaremos de 3 variáveis, uma para salvar o primeiro valor, outra para o segundo valor e a terceira para salvar qual operação está sendo selecionada. Então vamos criá-las: .DATA? .... Valor1 DWORD ? Valor2 DWORD ? Resultado DWORD ? ;; aqui é uma variável que vai ser usada mais a frente para salvar o ;;resultado da adição, subtração, Multiplicação ou Divisão. Operacao DWORD ?
Continuando com o botão de adição... Mov Operacao, 01h representada
;; Nesse momento definimos que a operação atual é adição
;;pelo número 1 Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100
Voltamos a usar nossa variável Edit_Texto em outro lugar, mas não se preocupe, trabalhar com ela aqui não vai atrapalhar o funcionamento dos outros botões. Agora
chegamos num ponto onde chamaremos uma função de conversão, porque teremos que converter os dados de string para dword. Esta função se encontra na biblioteca chamada masm32.lib e é só você adicionar lá no começo no cabeçalho "include \masm32\include\masm32.inc" e "includelib \masm32\lib\masm32.lib". Agora podemos chamar as funções "atodw" e "dwtoa", abreviações de "AsciiToDword" e "DwordtoAscii". Para chamar a função "atodw" nós passamos como parâmetro a string a ser convertida e o valor é retornado em eax. Invoke atodw, ADDR Edit_Texto ;; Movendo o valor convertido para a Mov Valor1, eax variável Valor1 Invoke SetWindowText, EditHandle, NULL ;; Deixando o Edit vazio para a entrada do ;; segundo valor .endif
Este é o código para o botão da soma. Para o botão da subtração a operação será mudada de 01h para 02h, e quando o clique for no botão multiplicar, 03h e é claro, quando o botão clicado for dividir 04h. Então programe O código dos outros botões estarão no fim do tutorial quando for mostrado o código completo. Estamos chegando no fim deste tutorial onde programaremos o código do botão Igual. É onde chamaremos os quatro procedimentos de adição, subtração, multiplicação e divisão. Teremos o seguinte código: invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 invoke atodw, ADDR Edit_Texto mov Valor2, eax ;; O valor2 foi pego, então já podemos fazer a operação .if Operacao == 01h invoke Soma, Valor1, Valor2 .elseif Operacao == 02h invoke Subtracao, Valor1, Valor2 .elseif Operacao == 03h invoke Multiplicacao, Valor1, Valor2 .elseif Operacao == 04h
Na divisão temos que verificar se o valor digitado pelo usuário foi zero ("0"). Se sim, mostraremos uma mensagem dizendo que é impossível dividir por zero. Então crie duas variáveis, uma para ser o texto da mensagem e a outra o caption. O código segue abaixo. .if Valor2 == 0 invoke MessageBox, hWnd, ADDR Texto_Msg, ADDR Titulo_Msg, MB_OK + MB_ICONEXCLAMATION invoke SetWindowText, EditHandle, NULL .elseif invoke Divisao, Valor1, Valor2 .endif invoke Divisao, Valor1, Valor2
.endif
Não execute o programa agora, pois ele daria erros dizendo que os procedimentos não existem. Então vamos criá-los. Primeiros temos que add os prototypes no início do programa. ... Gerenciador_Janela proto :DWORD, :DWORD, :DWORD, :DWORD Soma proto :DWORD, :DWORD, :DWORD, :DWORD Subtracao proto :DWORD, :DWORD, :DWORD, :DWORD Multiplicacao proto :DWORD, :DWORD, :DWORD, :DWORD Divisao proto :DWORD, :DWORD, :DWORD, :DWORD
Nesse momento você pode terminar o procedimento GerenteMensagem. Nada mais será posto lá. Então, primeiro iremos criar o procedimento de soma, o procedimento abaixo pode ser digitado antes de end inicio e fora de qualquer "proc". Considerando que já temos os dois valores predefinidos, o que precisaremos fazer? Somente adicionar um valor a outro. Procurei preservar os valores de eax e ebx, então os "pushs" e "pops" serão usados: Soma proc Val1: DWORD, Val2: DWORD push eax push ebx xor eax, eax ;; Zerando os valores dos dois registradores para receberem xor ebx, ebx os valores Val1 e Val2 mov eax, Val1 ;; Agora fazendo a soma entre os dois mov ebx, Val2 add eax, ebx ;; Movendo o resultado para a variável que já criamos mov Resultado, eax ;; Agora como sabemos não podemos mostrar uma variável DWORD como texto ela tem ;; que ser convertida então usaremos de dword para string, na chamada passamos como ;;parâmetro o valor a ser convertido e a string que irá receber o valor. invoke dwtoa, Resultado, ADDR Edit_Texto invoke SetWindowText, EditHandle, ADDR Edit_Texto xor eax, eax mov Valor1, eax mov Valor2, eax mov Resultado, eax pop ebx pop eax ret Soma endp
Se você entendeu esse procedimento, os outros não mudam grande coisa. O da subtração (Subtracao proc Val1: DWORD, Val2: DWORD), ao invés de add eax,ebx vai usar sub eax,ebx; o da multipilicação (Multiplicacao proc Val1: DWORD, Val2: DWORD) vai usar mul ebx e o da divisão (Divisao proc Val1: DWORD, Val2: DWORD) vai usar div ebx.
Comentários Tutorial grande, né? Eu achei, mas não sei se você percebeu, ainda ficou faltando o código de dois botões, o CE e o C. Então os deixo como atividade para vocês fazerem. Dou uma dica, um deles apaga somente o Valor2 e o outro zera tudo. Então até o próximo tutorial. Espero que vocês tenham aprendido alguma coisa galera, blz? Segue abaixo o código fonte completo da Calculadora.
Código fonte completo .386 .MODEL Flat, StdCall Option casemap:none include include include include
\masm32\include\windows.inc \masm32\include\user32.inc \masm32\include\kernel32.inc \masm32\include\masm32.inc
includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\masm32.lib Gerenciador_Janela proto :DWORD, :DWORD, :DWORD, :DWORD Soma proto :DWORD, :DWORD Subtracao proto :DWORD, :DWORD Multiplicacao proto :DWORD, :DWORD Divisao proto :DWORD, :DWORD .DATA Titulo_Janela db "Calculadora em ASM", 0 Classe_Janela db "Form1",0 Edit_Classe db "Edit", 0 Botao_Classe db "Button", 0 Botao1_Titulo db "1",0 Botao2_Titulo db "2", 0 Botao3_Titulo db "3", 0 Botao4_Titulo db "4", 0 Botao5_Titulo db "5", 0 Botao6_Titulo db "6", 0 Botao7_Titulo db "7", 0 Botao8_Titulo db "8", 0 Botao9_Titulo db "9", 0 Botao0_Titulo db "0", 0 BotaoVirgulaTitulo db ",", 0 BotaoIgualTitulo db "=", 0 BotaoMaisTitulo db "+", 0 BotaoMenosTitulo db "-", 0
BotaoMultiplicarTitulo db "*", 0 BotaoDividirTitulo db "/", 0 BotaoCETitulo db "CE", 0 BotaoCTitulo db "C", 0 Texto_Msg db "É impossível dividir por zero!", 0 Titulo_Msg db "Aviso!", 0 .DATA? Handle_Janela DWORD ? LinhaComando DWORD ? EditHandle DWORD ? Botao1_Handle DWORD ? Botao2Handle DWORD ? Botao3Handle DWORD ? Botao4Handle DWORD ? Botao5Handle DWORD ? Botao6Handle DWORD ? Botao7Handle DWORD ? Botao8Handle DWORD ? Botao9Handle DWORD ? Botao0Handle DWORD ? BotaoVirgulaHandle DWORD ? BotaoMaisHandle DWORD ? BotaoMenosHandle DWORD ? BotaoMultiplicarHandle DWORD ? BotaoDividirHandle DWORD ? BotaoIgualHandle DWORD ? BotaoCEHandle DWORD ? BotaoCHandle DWORD ? Edit_Texto db 100 dup (?) Endereco_String DWORD ? Operacao DWORD ? Valor1 DWORD ? Valor2 DWORD ? Resultado DWORD ? .CODE Inicio: Invoke GetModuleHandle, NULL Mov Handle_Janela, eax Invoke GetCommandLine Mov LinhaComando, eax invoke Gerenciador_Janela, Handle_Janela, NULL, LinhaComando, SW_SHOWDEFAULT Invoke ExitProcess, 0 Gerenciador_Janela proc hInstance:DWORD, hInstAntiga:DWORD, LnComando:DWORD, Tipo_Janela:DWORD LOCAL wnd: WNDCLASSEX LOCAL Janela: HWND
LOCAL Mensagem: MSG mov wnd.cbSize, SIZEOF WNDCLASSEX mov wnd.style, CS_HREDRAW or CS_VREDRAW mov wnd.lpfnWndProc, offset GerenteMensagem mov wnd.cbClsExtra, NULL mov wnd.cbWndExtra, NULL push hInstance pop wnd.hInstance invoke LoadIcon, NULL, IDI_WINLOGO mov wnd.hIcon, eax mov wnd.hIconSm, eax invoke LoadCursor, NULL, IDC_ARROW mov wnd.hCursor, eax mov wnd.hbrBackground, COLOR_BTNFACE+1 COLOR_BTNFACE+1 mov wnd.lpszMenuName, NULL mov wnd.lpszClassName, OFFSET Classe_Janela invoke RegisterClassEx, ADDR wnd invoke CreateWindowEx, NULL, ADDR Classe_Janela, ADDR Titulo_Janela,WS_OVERLAPPEDWINDOW, 433 433, , 302 302, , 230 230, ,190 190, , NULL, NULL, hInstance, NULL mov Janela, eax invoke ShowWindow, Janela, SW_SHOWNORMAL invoke UpdateWindow, Janela .WHILE TRUE invoke GetMessage, ADDR Mensagem, NULL, 0,0 .BREAK .IF (eax < 1) invoke TranslateMessage, ADDR Mensagem invoke DispatchMessage, ADDR Mensagem .ENDW mov eax, Mensagem.wParam ret Gerenciador_Janela endp GerenteMensagem proc hWnd: DWORD DWORD, ,
uMsg: UINT, wParam: WPARAM, lParam: LPARAM
.IF uMsg==WM_DESTROY Invoke PostQuitMessage, NULL .ELSEIF uMsg == WM_CREATE invoke CreateWindowEx, NULL, ADDR Edit_Classe, NULL, WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or ES_AUTOHSCROLL, 8, 8, 193 193, , 21, 21, hWnd, NULL, Handle_Janela, NULL mov EditHandle, eax Invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao1_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 8, 32 32, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov Botao1_Handle, eax
invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao2_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 48 48, , 32 32, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov Botao2Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao3_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 88 88, , 32 32, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov Botao3Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao4_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 8, 64 64, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov Botao4Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao5_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 48 48, , 64 64, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov Botao5Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao6_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 88 88, , 64 64, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov Botao6Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao7_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 8, 96 96, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov Botao7Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao8_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 48 48, , 96 96, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov Botao8Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao9_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 88 88, , 96 96, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov Botao9Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR Botao0_Titulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 8, 128 128, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov Botao0Handle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoVirgulaTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 48 48, , 128 128, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov BotaoVirgulaHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoIgualTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 88 88, , 128 128, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov BotaoIgualHandle, eax
invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoMaisTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 128 128, , 32 32, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov BotaoMaisHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoMenosTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 128 128, , 64 64, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov BotaoMenosHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoMultiplicarTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 128 128, , 96 96, , 33, 33 , 25 25, , hWnd, NULL, Handle_Janela, NULL mov BotaoMultiplicarHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoDividirTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 128 128, , 128 128, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov BotaoDividirHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoCETitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 168 168, , 32 32, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov BotaoCEHandle, eax invoke CreateWindowEx, NULL, ADDR Botao_Classe, ADDR BotaoCTitulo, WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, 168 168, , 64 64, , 33 33, , 25 25, , hWnd, NULL, Handle_Janela, NULL mov BotaoCHandle, eax .ELSEIF uMsg==WM_COMMAND mov edx, lParam .if edx == Botao1_Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao1_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] Endereco_String] .endif .if edx == Botao2Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao2_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] Endereco_String] .endif .if edx == Botao3Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao3_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] Endereco_String] .endif
.if edx == Botao4Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao4_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] Endereco_String] .endif .if edx == Botao5Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao5_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] Endereco_String] .endif .if edx == Botao6Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao6_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] Endereco_String] .endif .if edx == Botao7Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao7_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] Endereco_String] .endif .if edx == Botao8Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao8_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] Endereco_String] .endif .if edx == Botao9Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao9_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] Endereco_String] .endif .if edx == Botao0Handle Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke lstrcat, ADDR Edit_Texto, ADDR Botao0_Titulo Mov Endereco_String, eax Invoke SetWindowText, EditHandle, [Endereco_String] Endereco_String] .endif .if edx == BotaoMaisHandle Mov Operacao, 01h Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke atodw, ADDR Edit_Texto Mov Valor1, eax Invoke SetWindowText, EditHandle, NULL
.endif .if edx == BotaoMenosHandle Mov Operacao, 02h Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke atodw, ADDR Edit_Texto Mov Valor1, eax Invoke SetWindowText, EditHandle, NULL .endif .if edx == BotaoMultiplicarHandle Mov Operacao, 03h Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke atodw, ADDR Edit_Texto Mov Valor1, eax Invoke SetWindowText, EditHandle, NULL .endif .if edx == BotaoDividirHandle Mov Operacao, 04h Invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 Invoke atodw, ADDR Edit_Texto Mov Valor1, eax ;;Movendo o valor convertido para a variável Valor1 Invoke SetWindowText, EditHandle, NULL .endif .if edx == BotaoIgualHandle invoke GetWindowText, EditHandle, ADDR Edit_Texto, 100 invoke atodw, ADDR Edit_Texto mov Valor2, eax .if Operacao == 01h Invoke Soma, Valor1, Valor2 .elseif Operacao == 02h Invoke Subtracao, Valor1, Valor2 .elseif Operacao == 03h Invoke Multiplicacao, Valor1, Valor2 .elseif Operacao == 04h .if Valor2 == 0 invoke MessageBox, hWnd, ADDR Texto_Msg, ADDR Titulo_Msg, MB_OK + MB_ICONEXCLAMATION invoke SetWindowText, EditHandle, NULL .elseif Invoke Divisao, Valor1, Valor2 .endif .endif .endif .ELSE invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret
.ENDIF
xor eax, eax ret GerenteMensagem endp Soma proc Val1: DWORD, Val2: DWORD Push eax Push ebx Xor eax, eax Xor ebx, ebx Mov eax, Val1 Mov ebx, Val2 Add eax, ebx Mov Resultado, eax Invoke dwtoa, Resultado, ADDR Edit_Texto Invoke SetWindowText, EditHandle, ADDR Edit_Texto Xor eax, eax Mov Valor1, eax Mov Valor2, eax Mov Resultado, eax Pop ebx Pop eax ret Soma endp Subtracao proc Val1: DWORD, Val2: DWORD Push eax Push ebx Xor eax, eax Xor ebx, ebx ;;Zerando os valores dos dois registradores para receberem os valores Val1 e Val2 Mov eax, Val1 Mov ebx, Val2 ;; Agora fazendo a soma entre os dois SUB eax, ebx Mov Resultado, eax Invoke dwtoa, Resultado, ADDR Edit_Texto Invoke SetWindowText, EditHandle, ADDR Edit_Texto Xor eax, eax Mov Valor1, eax Mov Valor2, eax Mov Resultado, eax Pop ebx Pop eax ret Subtracao endp Multiplicacao proc Val1: DWORD, Val2: DWORD Push eax Push ebx Xor eax, eax Xor ebx, ebx Mov eax, Val1 Mov ebx, Val2
MUL ebx Mov Resultado, eax Invoke dwtoa, Resultado, ADDR Edit_Texto Invoke SetWindowText, EditHandle, ADDR Edit_Texto Xor eax, eax Mov Valor1, eax Mov Valor2, eax Mov Resultado, eax Pop ebx Pop eax ret Multiplicacao endp Divisao proc Val1: DWORD, Val2: DWORD push eax push ebx xor eax, eax xor ebx, ebx mov eax, Val1 mov ebx, Val2 Div ebx mov Resultado, eax invoke dwtoa, Resultado, ADDR Edit_Texto invoke SetWindowText, EditHandle, ADDR Edit_Texto xor eax, eax mov Valor1, eax mov Valor2, eax mov Resultado, eax pop ebx pop eax ret Divisao endp end Inicio
Índice do Artigo Objetos e Manipuladores (handles) Objetos kernel Todas as páginas Um objeto é uma estrutura interna que representa um recurso do sistema, como um arquivo, uma linha de processo ( thread) ou uma imagem gráfica. Um aplicativo não pode acessar diretamente a estrutura interna de um objeto ou do recurso do sistema que este objeto representa - precisa obter um manipulador (handle) para poder examiná-lo e/ou modificá-lo. O Windows usa objetos e manipuladores para vigiar o acesso aos recursos do sistema.
Isto impede que programadores tenham acesso direto às estruturas internas de baixo nível. Por outro lado, cada objeto tem sua própria lista de controle de acessos ( ACL access-control list) que especifica os tipos de ação permitidos para cada objeto. O sistema operacional examina a ACL de um objeto toda vez que um aplicativo tentar criar um manipulador para ele. Para a maioria dos objetos, a API Win32 oferece funções para criá-los, criar seus manipuladores, fechá-los e destruí-los. Manipuladores e objetos consomem memória. Para preservar a performance do sistema, um aplicativo deve fechar manipuladores e destruir objetos que não sejam mais necessários. O Windows oferece três categorias de objetos: de ususário (user), de interface de dispositivos gráficos (GDI) e dekernel. O sistema utiliza objetos user para apoiar o gerenciamento de janelas, objetos GDI para apoiar gráficos e objetos kernel para apoiar o gerenciamento da memória, a execução do processo e a intercomunicação de processos. Alguns objetos, como os user e GDI, só podem ter um manipulador. O sistema fornece um manipulador quando o aplicativo criar o objeto e invalida o manipulador quando o aplicativo destruir o objeto. Outros objetos, como alguns do kernel, podem possuir vários manipuladores. O sistema operacional remove o objeto da memória assim que o último manipulador for fechado. O número de manipuladores abertos depende unicamente do total de memória disponível. Entretanto, um único processo não pode ter mais do que 16.384 manipuladores de objetos GDI abertos simultaneamente. O limite de manipuladores de kernel é de 2 30 por processo. Para manipuladores user não há limite por processo, mas há o limite de 65.536 do sistema.
Objetos USER e GDI Os objetos user e GDI podem ter apenas um manipulador. O processo não pode herdar ou duplicar estes manipuladores. Os manipuladores user são públicos para todos os processos, ou seja, qualquer processo pode usar um manipulador de um objeto user existente. Os manipuladores de objetos GDI são privativos de um processo, ou seja, apenas o processo que criou o objeto GDI pode usar seu manipulador. Na figura abaixo um aplicativo cria um objeto janela(1). A função CreateWindow cria um objeto janela (2) e retorna o manipulador deste objeto (3). Este manipulador do objeto janela pode ser utilizado pelo aplicativo para mostrar ou alterar a janela. O manipulador é válido enquanto o objeto correspondente não for destruído.
Na figura a seguir, o aplicativo destrói o objeto janela(1). A função DestroyWindow retira o objeto janela da memória (2), o que invalida o manipulador (3).
A tabela abaixo contém uma lista de objetos user e as funções correspondentes para criá-los e destruí-los. As funções de criação criam o objeto e seu manipulador ou então retornam simplesmente um manipulador existente. As funções de destruição removem o objeto da memória, o que invalida o manipulador correspondente, com exceção dos objetos window station e desktop, que são criados e mantidos pelo sistema (um aplicativo não consegue destruir estes dois objetos).
Funções para objetos USER OBJETO USER
Função para criar
Função para destruir
Tabela de Aceleradores
CreateAcceleratorTable
DestroyAcceleratorTable
Cursor
CreateCursor, LoadCursor, GetCursor, SetCursor
DestroyCursor
Conversação DDE
DdeConnect, DdeConnectList, DdeQueryNextServer, DdeReconnect
DdeDisconnect, DdeDisconnectList
Desktop
GetThreadDesktop
Hook
SetWindowsHook, SetWindowsHookEx
UnhookWindowsHook, UnhookWindowsHookEx
Menu
CreateMenu, CreatePopupMenu, GetMenu, GetSubMenu,
DestroyMenu
GetSystemMenu, LoadMenu, LoadMenuIndirect
Janela
CreateWindow, CreateWindowEx, CreateDialogParam, CreateDialogIndirectParam, CreateMDIWindow, FindWindow, GetWindow, GetClipboardOwner, GetDesktopWindow, GetDlgItem, GetForegroundWindow, GetLastActivePopup, GetOpenClipboardWindow, GetTopWindow, WindowFromDC, WindowFromPoint e outras
DestroyWindow
Posição da Janela
BeginDeferWindowPos
EndDeferWindowPos
Window station
GetProcessWindowStation
Funções para objetos GDI OBJETO GDI
Função para criar
Função para destruir
Bitmap
CreateBitmap, CreateBitmapIndirect, CreateCompatibleBitmap, CreateDlBitmap, CreateDIBSection, CreateDiscardableBitmap
DeleteObject
Brush (pincel)
CreateBrushIndirect, CreateDIBPatternBrush, CreateDIBPatternBrushPt, CreateHatchBrush, CreatePatternBrush, CreateSolidBrush
DeleteObject
Fonte
CreateFont, CreateFontIndirect
DeleteObject
Paleta
CreatePalette
DeleteObject
Pen (pena)
CreatePen, CreatePenIndirect
DeleteObject
Pena Extendida
ExtCreatePen
DeleteObject
Região
CombineRgn, CreateEllipticRgn, CreateEllipticRgnIndirect, CreatePolygonRgn, CreatePolyPoligonRgn, CreateRectRgn, CreateRectRgnIndirect, CreateRoundRectRgn, ExtCreateRegion, PathToRegion
DeleteObject
Contexto de Dispositivo (DC)
CreateDC, GetDC
DeleteDC, ReleaseDC
DC de Memória
CreateCompatibleDC
DeleteDC
Metafile
CloseMetaFile, CopyMetaFile, GetMetaFile, SetMetaFileBitsEx
DeleteMetaFile
DC de Metafile
CreateMetafile
CloseMetaFile
Enhanced metafile
CloseEnhMetaFile, CopyEnhMetaFile, GetEnhMetaFile, SetEnhMetaFileBits
DeleteEnhMetaFile
DC de enhanced metafile
CreateEnhMetaFile
CloseEnhMetaFile
Objetos kernel Os objetos kernel são específicos do processo. Isto significa que um processo precisa criar um objeto kernel ou então abrir um objeto existente para obter seu manipulador. Qualquer processo pode criar um manipulador novo para um objeto kernel existente, mesmo para um que já tenha sido criado por outro processo, contanto que o processo tenha o nome do objeto e tenha acesso seguro a ele. Manipuladores de objetos kernel incluem direitos de acesso que indicam as ações permitidas ou proibidas a um processo. Um aplicativo especifica os direitos de acesso quando cria um objeto ou obtém um manipulador existente. Cada tipo de objeto kernel dá suporte a seu próprio conjunto de direitos de acesso. Por exemplo, manipuladores de evento podem ter autorização para "atribuir" (set), "esperar" (wait) ou ambos; manipuladores de arquivos podem estar autorizados para "leitura", "escrita" ou ambos, e assim por diante. Processos podem herdar ou duplicar manipuladores para os seguintes tipos de objetos kernel, entre outros:
Processos Linhas de processo (threads) Arquivos (inclusive objetos de arquivos mapeados) Eventos Semáforos Mutex Pipes (nominados e anônimos) Mailslots Dispositivos de comunicação
Na ilustração a seguir, um aplicativo cria um objeto evento. A função CreateEvent cria o objeto evento e retorna uma manipulador deste objeto:
(1) Um aplicativo executa a função CreateEvent , a qual cria um objeto evento (2) na memória. A função retorna o manipulador deste objeto (3). Após o objeto evento ter sido criado, o aplicativo pode usar o manipulador de evento para atribuir ou esperar pelo evento. O manipulador continua disponível até que o aplicativo o destrua ou até que o aplicativo termine. A maioria dos objetos kernel permitem múltiplos manipuladores atrelados a um único objeto. Por exemplo, o aplicativo da ilustração anterior poderia obter manipuladores de objetos de evento adicionais usando a função OpenEvent , como mostrado a seguir:
(4) O aplicativo executa a função OpenEvent , que retorna um (5) novo manipulador de evento. Este método permite que um aplicativo tenha dois manipuladores com direitos de acesso diferentes. Por exemplo, o manipulador (3) pode ter os direitos "atribuir" e "esperar" e o manipulador (5) ter apenas o direito "esperar". Se outro processo tiver o nome e o acesso seguro ao objeto, ele pode criar seu próprio manipulador de objeto evento usando OpenEvent . O aplicativo que criou o primeiro manipulador também pode duplicar um dos seus manipuladores para o mesmo processo ou para um outro processo através da função DuplicateHandle. Um objeto permanece na memória enquanto pelo menos um manipulador deste objeto ainda existir. Na ilustração seguinte, os aplicativos usam a função CloseHandle para fechar seus manipuladores do objeto evento. Quando não existirem mais manipuladores de evento, o sistema remove o objeto da memória:
O sistema trata objetos arquivo de uma forma diferenciada. Os objetos arquivo possuem um ponteiro de arquivo - o ponteiro para o próximo byte a ser lido ou escrito no arquivo. Sempre que um aplicativo criar um novo manipulador de arquivo, o sistema cria um novo objeto arquivo. Portanto, mais de um objeto arquivo pode referenciar o mesmo arquivo em disco, como mostrado abaixo:
Apenas através de duplicação ou de herança, mais de um manipulador de arquivo pode referenciar o mesmo objeto arquivo, como mostrado a seguir:
A próxima tabela mostra cada um dos objetos kernel com suas funções de criação e destruição correspondentes. As funções de criação criam um objeto e seu respectivo manipulador ou criam um novo manipulador para um objeto existente. Quando o aplicativo fecha o último manipulador de um objeto, o sistema remove o objeto da memória.
OBJETO KERNEL
Função para criar
Função para destruir
Alteração de notificação
FindFirstChangeNotification
FindCloseChangeNotification
Arquivo
CreateFile
CloseHandle, DeleteFile
Atualizar recurso
BeginUpdateResource
EndUpdateResource
Buffer de tela de console
CreateFile com CONOUT$
CloseHandle
Desktop
GetThreadDesktop
Aplicativos não podem eliminar este objeto
Dispositivo de comunicação
CreateFile
CloseHandle
Entrada de console
CreateFile com CONIN$
CloseHandle
Estação Windows
GetProcessWindowStation
Aplicativos não podem eliminar este objeto
Evento
CreateEvent, OpenEvent
CloseHandle
Heap
HeapCreate
HeapDestroy
Log de evento
OpenEventLog, RegisterEventSource, OpenBackupEventLog
CloseEventLog
Mailslot
CreateMailslot
CloseHandle
Mapeamento de arquivo
CreateFileMapping, OpenFileMapping
CloseHandle
Módulo
LoadLibrary, GetModuleHandle
FreeLibrary
Mutex
CreateMutex, OpenMutex
CloseHandle
Notificação de recurso de memória
CreateMemoryResourceNotification
CloseHandle
Pipe
CreateNamedPipe, CreatePipe
CloseHandle, DisconnectNamedPipe
Processo
CreateProcess, OpenProcess, GetCurrentProcess
CloseHandle, TerminateProcess
Procurar arquivo
FindFirstFile
FindClose
Semáforo
CreateSemaphore, OpenSemaphore
CloseHandle
Socket
socket, accept
CloseHandle
Tarefa
CreateJobObject
CloseHandle
Thread
CreateThread, CreateRemoteThread, GetCurrentThread
CloseHandle, TerminateThread
Timer
CreateWaitableTimer, OpenWaitableTimer
CloseHandle
Token de acesso
CreateRestrictedToken, DuplicateToken, DuplicateTokenEx, OpenProcessToken, OpenThreadToken
CloseHandle
Índice do Artigo Assembly - Tratamento de erros Moldura de pilha Todas as páginas
O tratamento adequado de erros é uma ciência à parte, talvez até mais complexa do que a própria programação. Durante a programação, toda a atenção está voltada para eventos previsíveis, o objetivo do programa. No tratamento de erros, no entanto, lidamos com eventos indesejados, quase que "imprevisíveis". É como brincar com uma bola de cristal Neste texto serão abordados apenas os mecanismos envolvidos no caso de ocorrência de erros. Serve apenas como orientação para a aplicação de um sistema eficiente de um tratamento estruturado de erros, indispensável para garantir a robustez e a consistência de aplicativos mais elaborados.
Erros Denominamos erro ou exceção um evento que ocorre durante a execução de um programa e que requer uma execução fora do fluxo normal de controle. Existem exceções de hardware e de software. Exceções de hardware podem ser resultado da execução de sequências de instruções que tentam acessar endereços de memória inválidos ou efetuar uma divisão por zero. Exceções de software podem ser resultado do uso de parâmetros com valores inválidos ou serem iniciadas explicitamente através do uso da função RaiseException. Em todo caso, são "pecados" cometidos contra o sistema - o "mau comportamento" que gera uma exceção pode comprometer o funcionamento do sistema operacional o qual, para se precaver, interrompe o processo e apresenta uma mensagem de erro do tipo:
Tratamento Estruturado de Exceções A API do Windows oferece um mecanismo próprio para o tratamento de exceções geradas tanto por hardware quanto por software: é o chamado tratamento estruturado de exceções. Este mecanismo permite um controle absoluto no tratamento de exceções, oferece suporte para depuradores (debuggers) e pode ser usado em todas as linguagens de programação e em todo tipo de máquina. A API do win32 também permite a manipulação de conclusão. Este tipo de manipulação garante que, sempre que uma área de código vigiado for executada, um bloco de código de conclusão específico também seja executado, tenha ou não ocorrido uma exceção. O código de conclusão é executado independentemente da maneira como o fluxo de controle tenha saído da área vigiada. Por exemplo, o manipulador de conclusão pode garantir que determinadas tarefas, como as de "faxina", sejam cumpridas mesmo que ocorra uma exceção ou algum outro erro durante a execução do código da área vigiada.
Se você programa em "C/C++" ou "Delphi", com certeza conhece as plavraschave try, except e finally. Try (tentar) identifica a área vigiada, except (exceto) identifica o manipulador de exceções e finally (no final) o manipulador de conclusão. Pondo em linguagem corrente seria o mesmo que "tente executar o código desta área; caso ocorra algum erro, faça uso do manipulador de exceções; no final, acione o manipulador de conclusão". Esta estrutura de atendimento permite que se programe aplicativos mais robustos e confiáveis e é conhecida como SEH ou Structured Exception Handling.
Tipos de Exceção Como já foi citado, as exceções podem ser iniciadas pelo hardware ou pelo software. Tanto uma quanto a outra podem ocorrer no modo kernel (no "interior" do sistema) ou no código modo usuário (o código que você programou). Daí os tipos de exceção:
de hardware no modo kernel de hardware no modo usuário de software no modo kernel de software no modo usuário
Estruturas das Exceções Quando ocorre uma exceção, o processador pára a execução no ponto onde ela ocorreu e transfere o controle para o sistema. A primeira providência do sistema é a de guardar o "estado de máquina" da linha de processo atual (o thread atual) e as informações que descrevem a exceção. Logo a seguir, o sistema procura por um manipulador de exceções para tratar o erro. O "estado de máquina" é armazenado numa estrutura do tipo CONTEXT. Esta informação, denominada registro do contexto (context record), vai permitir que o sistema continue a execução a partir do ponto gerador da exceção se esta for tratada com sucesso. A descrição da exceção, chamada de registro da exceção (exception record), é armazenada numa estrutura do tipo EXCEPTION_RECORD. Devido ao fato das informações referentes à máquina e as informações referentes à exceção serem guardadas em estruturas diferentes, o mecanismo de tratamento de exceções torna-se portável para as mais diversas plataformas.
A estrutura EXCEPTION_RECORD A definição desta estrutura é: typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation [EXCEPTION_MAXIMUM_PARAMETERS]; }
O membro DWORD ExceptionFlags pode ser zero, indicando uma exceção recuperável, ou ser EXCEPTION_NONCONTINUABLE, que indica uma exceção irrecuperável. Qualquer tentativa de continuar a execução após uma exceção irrecuperável causa uma nova exceção do tipo EXCEPTION_NONCONTINUABLE_EXCEPTION. O membro struct _EXCEPTION_RECORD *ExceptionRecord aponta para uma estrutura do tipo EXCEPTION_RECORD. Os registros de exceção podem ser encadeados para fornecerem informações adicionais quando ocorrer uma exceção aninhada. O membro PVOID ExceptionAddress especifica o endereço onde ocorreu a exceção. O membro DWORD NumberParameters especifica o número de parâmetros associados à exceção. Este é o número de elementos definidos na matriz (array) ExceptionInformation. O membro DWORD ExceptionInformation [EXCEPTION_MAXIMUM_PARAMETERS] especifica uma matriz com argumentos de 32 bits adicionais que descrevem a exceção. A função RaiseException pode especificar esta matriz de argumentos. Para a maior parte dos códigos de exceção os elementos da matriz estão indefinidos. O membro DWORD ExceptionCode indica a razão pela qual a exceção ocorreu. Os valores possíveis estão na tabela abaixo:
EXCEPTION_ACCESS_VIOLATION
O thread tentou ler/escrever num endereço virtual ao qual não tinha acesso.
EXCEPTION_ARRAY_BOUNDS_EXCEEDED
O thread tentou acessar um elemento de array fora dos limites e o hardware possibilita a checagem de limites.
EXCEPTION_BREAKPOINT
Foi encontrado um ponto de parada (breakpoint).
EXCEPTION_DATATYPE_MISALIGNMENT
O thread tentou ler/escrever dados desalinhados em hardware que não oferece alinhamento. Por exemplo, valores de 16 bits precisam ser alinhados em limites de 2 bytes; valores de 32 bits em limites de 4 bytes, etc.
EXCEPTION_FLT_DENORMAL_OPERAND
Um dos operandos numa operação de ponto flutuante está desnormatizado. Um valor desnormatizado é um que seja pequeno demais para poder ser representado no formato de ponto flutuante padrão.
EXCEPTION_FLT_DIVIDE_BY_ZERO
O thread tentou dividir um valor em ponto flutuante por um divisor em ponto flutuante igual a zero.
EXCEPTION_FLT_INEXACT_RESULT
O resultado de uma operação de ponto flutuante não pode ser representado como uma fração decimal exata.
EXCEPTION_FLT_INVALID_OPERATION
Qualquer operação de ponto flutuante não incluída na lista.
EXCEPTION_FLT_OVERFLOW
O expoente de uma operação de ponto flutuante é maior que a magnitude permitida pelo tipo correspondente.
EXCEPTION_FLT_STACK_CHECK
A pilha ficou desalinhada ("estourou" ou "ficou abaixo") como resultado de uma operação de ponto flutuante.
EXCEPTION_FLT_UNDERFLOW
O expoente de uma operação de ponto flutuante é menor que a magnitude permitida pelo tipo correspondente.
EXCEPTION_ILLEGAL_INSTRUCTION
O thread tentou executar uma instrução inválida.
EXCEPTION_IN_PAGE_ERROR
O thread tentou acessar uma página que não estava presente e o sistema não foi capaz de carregar a página. Esta exceção pode ocorrer, por exemplo, se uma conexão de rede é perdida durante a execução do
programa via rede.
EXCEPTION_INT_DIVIDE_BY_ZERO
O thread tentou dividir um valor inteiro por um divisor inteiro igual a zero.
EXCEPTION_INT_OVERFLOW
O resultado de uma operação com inteiros causou uma transposição (carry) além do bit mais significativo do resultado.
EXCEPTION_INVALID_DISPOSITION
Um manipulador (handle) de exceções retornou uma disposição inválida para o tratador de exceções. Uma exceção deste tipo nunca deveria ser encontrada em linguagens de médio/alto nível.
O thread tentou continuar a execução após a ocorrência EXCEPTION_NONCONTINUABLE_EXCEPTION de uma exceção irrecuperável.
EXCEPTION_PRIV_INSTRUCTION
O thread tentou executar uma instrução cuja operação não é permitida no modo de máquina atual.
EXCEPTION_SINGLE_STEP
Um interceptador de passos ou outro mecanismo de instrução isolada sinalizou que uma instrução foi executada.
EXCEPTION_STACK_OVERFLOW
O thread esgotou sua pilha (estouro de pilha).
Quando ocorre uma exceção no código modo usuário o sistema realiza a seguinte procura por um manipulador de exceções: 1. Primeiro, o sistema tenta notificar o depurador do processo (debugger), se este existir. 2. Se o processo não estiver sendo depurado ou se o depurador não tratar a exceção, o sistema tenta localizar um manipulador de exceções emolduradas (frame-based) fazendo uma procura nas molduras da pilha do thread onde ocorreu a exceção. O sistema procura primeiro na moldura atual da pilha, depois segue procurando em molduras de pilha precedentes. (Leia sobre molduras de pilha logo abaixo). 3. Se não foi possível encontrar um manipulador emoldurado ou se nenhum deles tratar a exceção, o sistema faz uma segunda tentativa de notificar o depurador do
processo. 4. Se o processo não estiver sendo depurado ou se o depurador associado não tratar a exceção, o sistema fornece um tratamento padrão de acordo com o tipo de exceção. Para a maioria das exceções, a ação padrão é chamar a função ExitProcess. Quando ocorre uma uma exceção no código modo kernel, o sistema procura nas molduras da pilha do kernel na tentativa de localizar um manipulador de exceções. Se o manipulador não puder ser encontrado ou se nenhum dos manipuladores tratar a exceção, o sistema encerra sua operação como se a função ExitProcess tivesse sido chamada.
Moldura de Pilha Imagine uma função A que chama uma função B que chama uma função C. Cada uma destas funções possui um manipulador de exceções próprio, ou seja, possuem áreas vigiadas. Quando a função A é chamada, o aspecto da pilha é o seguinte: Endereço de retorno da Função B Uso da pilha pela função A ... Manipulador de Exceções A Dados Locais da Função A Endereço de retorno da Função A ... Após a chamada das funções B e C, o aspecto da pilha fica assim: Uso da pilha pela função C ... Manipulador de Exceções C Dados Locais da Função C Endereço de retorno da Função C Uso da pilha pela função B ... Manipulador de Exceções B Dados Locais da Função B Endereço de retorno da Função B Uso da pilha pela função A ... Manipulador de Exceções A Dados Locais da Função A
Endereço de retorno da Função A ... As molduras de pilha são os endereços da pilha que pertencem a um determinado processo. Caso a função C e a função B sejam encerradas, a moldura da função C e a moldura da função B serão retiradas e a pilha volta a ter o aspecto do primeiro exemplo, onde a moldura da função A volta a ser a moldura do topo da pilha. Este processo é o ALINHAMENTO de pilha. Se ocorrer uma exceção na área vigiada da função C, o manipulador de exceções C é "engatilhado". Se este manipulador não tratar da exceção, o sistema procura o manipulador B e o "engatilha". Caso este também não trate da exceção, o sistema continua a procura e acha o manipulador A, que também é "engatilhado". Fica claro que o sistema "caminha pilha abaixo", percorrendo a cadeia de manipuladores. Digamos que o manipulador A trate a exceção. Neste caso ele passa de "engatilhado" para "desarmado". A partir daí, já que o problema foi resolvido, os outros manipuladores também precisam ser "desarmados". O manipulador A se encarrega de "desarmar" todos os manipuladores anteriores a ele. Começa pelo manipulador C, faz o mesmo caminho pilha abaixo e "desarma" toda a cadeia de manipuladores. Este processo chama-se stack unwind , literalmente, "desdobramento da pilha".
Tratamento de Exceções Emolduradas (Frame-based) Um manipulador de exceções emoldurado é um mecanismo através do qual o programador lida com a possibilidade de que uma exceção possa ocorrer numa determinada sequência de código. Um manipulador de exceções emoldurado é composto pelos seguintes elementos:
Uma área de código vigiada Uma expressão de filtro Um bloco de tratamento de exceções
Em linguagens de nível mais alto que o assembly, como o C/C++ por exemplo, os manipuladores de exceções emolduradas são implementados pelas declarações tryexcept . A área de código vigiada é constituída por uma ou mais declarações para as quais a expressão de filtro e o bloco de tratamento de exceções fornece proteção. A área de código pode ser um bloco de código, um conjunto de blocos aninhados ou a totalidade de um procedimento ou função. A expressão de filtro é uma expressão que é avaliada pelo sistema caso ocorra uma exceção na área vigiada. A avaliação resulta em uma das seguintes ações do sistema:
O sistema interrompe a sua procura por um manipulador de exceções, restaura o estado da máquina e continua a execução do thread no ponto onde a exceção ocorreu. O sistema continua procurando um manipulador de exceções.
O sistema transfere o controle para o manipulador de exceções e a execução do thread continua sequencialmente na moldura de pilha na qual o manipulador foi encontrado. Se o manipulador de exceções não se encontrar na moldura de pilha na qual a exceção ocorreu, o sistema realinha a pilha abandonando a moldura de pilha atual e quaisquer outras molduras até que retorne à moldura de pilha onde se encontra o manipulador de exceções.
O bloco de tratamento das exceções pode ser tão simples quanto apenas anotar o erro e ativar um indicador que será analisado posteriormente ou imprimir uma mensagem de erro ou um aviso. Se a execução continuar, pode ser necessário alterar o estado de máquina modificando o registro de contexto (context record).
Observação da vó Gostou do assunto? Então, se for do tipo maluco beleza, dê uma lida no tutorial "Lidando com exceções".
Índice do Artigo Assembly - Lidando com exceções Manipuladores de exceção Informação para os manipuladores Reparando uma exceção Programas de teste Todas as páginas Exceções são "pecados" que os programas ou o hardware cometem. O código "mal comportado" que gera uma exceção pode comprometer o funcionamento do sistema operacional o qual, para se precaver, geralmente interrompe o programa faltoso e apresenta uma mensagem de erro do tipo
Ao invés de delegar ao sistema a função de monitorar exceções, podemos gerenciá-las por conta própria, tornando nossos aplicativos mais robustos. Veja como fazer isto.
Entendendo a manipulação de exceções A idéia básica na manipulação de exceções, também denominada Manipulação Estruturada de Exceções ou SEH , é a de codificar uma ou várias rotinas callback no aplicativo. Estas rotinas são denominadas genericamente de manipuladores de exceção (exception handlers). Se ocorrer uma exceção, o sistema, ao invés de tratá-la, chamará a rotina callback e a responsabilidade de tratá-la ficará por conta do aplicativo. O que se espera é que o manipulador de exceções seja capaz de resolver e corrigir a exceção, mantendo a execução do aplicativo na mesma área de código onde a exceção ocorreu ou numa "área segura". Em resumo, deve dar a impressão de que nenhuma anomalia tenha ocorrido - nada de caixa de mensagens. Além disto, o tratamento de erros pode realizar uma bela faxina: incluir o fechamento de manipuladores, fechamento de arquivos temporários, liberação de modelos de contexto, liberação de áreas de memória, informação a outros threads, ajuste de pilha ou o fechamento do thread "pecador". Durante o processo, o manipulador de exceções pode registrar em arquivo todas as fases do tratamento do erro, possibilitando uma análise posterior. Caso não seja possível realizar um tratamento adequado, o manipulador de exceções ainda pode fechar o aplicativo de forma mais elegante que a famigerada "janelinha de erro" após realizar o máximo de faxina, preservar o máximo de dados e, caso você queira, pedindo as devidas desculpas pelo transtorno.
O que é possível fazer Primeiro a boa notícia: existem as mais diversas aplicações para um manipulador de exceções. Dentre elas, destacam-se as seguintes:
Durante o desenvolvimento de um programa pode interceptar e registrar erros e funcionar como uma alternativa de debug. Quando usarmos código escrito por terceiros que ainda não tenham sido adequadamente testado. Quando realizarmos leitura ou escrita em áreas de memória que possam ter mudado sem aviso prévio. Por exemplo, quando fuçamos em áreas de memória do sistema (que deveriam estar sob a tutela do sistema) ou em áreas de memória que possam ser fechadas por outros processos ou threads. Usando ponteiros de arquivos que possam estar corrompidos ou em formatos incorretos. Neste caso, um manipulador de exceções é muito mais rápido do que o uso das funções da API IsBadReadPtr ou IsBadWritePtr , onde cada novo ponteiro precisa ser testado antes de ser usado. Como um interceptador geral de erros involuntários.
O que não é possível prevenir Agora, a má notícia: existem erros que geram exceções irrecuperáveis. O tipo mais
comum, não levando em consideração a divisão por zero (código de exceção 0C0000094h), que pode ser facilmente evitada através de codificação de proteção, é a tentativa de leitura ou escrita num endereço de memória inválido (código de exceção 0C0000005h). As origens deste erro são diversas:
Valores errados do registrador de índice quando se endereça a memória. Loops contínuos inesperados que envolvam acesso à memória. PUSH e POP descasados, de modo que, retornando de uma chamada, a continuidade da execução ocorre a partir de um lugar errado. Corrupção inesperada de arquivos de dados de entrada.
Em todos estes casos o imponderável é o fator determinante do erro. Não nos resta outra alternativa a não ser fazer o máximo de faxina e encerrar o aplicativo com as devidas desculpas. Existem outras causas de interrupção de aplicativos, porém não estão associadas a exceções. As mais comuns são:
Recursos insuficiente do sistema. Loops contínuos que não envolvam acesso de memória.
O resultado, nestes casos, é que o programa não pode responder as mensagens do sistema e dá a impressão de estar parado (congelado ou pendurado). Como o programa roda no seu espaço de endereçamento virtual próprio, outros programas não são afetados. O sistema, porém, fica mais lento. Alguns erros são tão graves que o sistema nem consegue redirecionar o tratamento da exceção para o manipulador. Neste caso, ou aparece a "janelinha de erro" ou... a tela azul do GPF mostrando um "erro fatal". Neste caso de desastre total, na maioria das vezes, o único remédio é fazer um reboot.
Como o sistema trata as exceções Para poder criar manipuladores de exceção, é óbvio que precisamos conhecer a rotina de manipulação de exceções do sistema. 1. Em primeiro lugar, o Windows decide se a exceção deve ser tratada pelo manipulador de exceções do programa. Se for o caso, verifica se o programa está sendo "debugado". Se sim, o sistema notifica o debugger suspendendo a execução do programa e enviando um EXCEPTION_DEBUG_EVENT (valor 01h). 2. Se o programa não estiver sendo "debugado", ou se a exceção não for compartilhada com o debugger, o sistema envia a exceção ao seu manipulador de exceções thread-específico - se é que existe um manipulador. Um manipulador thread-específico é instalado em tempo de execução e apontado pelo primeiro dword no Bloco de Informações de Thread (Thread Information Block), cujo endereço está em FS:[0]. 3. O manipulador de exceções thread-específico pode tentar resolver a exceção ou então então deixá-la para outros manipuladores hierarquicamente superiores, se é que há mais manipuladores instalados. 4. Eventualmente, se nenhum dos manipuladores thread-específicos tratar a
exceção e se o programa estiver sendo debugado, o sistema suspenderá novamente a execução do programa e notificará o debugger. 5. Se o programa não estiver sendo debugado ou se a exceção ainda não tiver sido tratada pelo debugger, o sistema chamará seu manipulador final se este estiver instalado. Este será um manipulador final instalado pelo aplicativo em tempo de execução e utilizando a função da API SetUnhandledExceptionFilter . 6. Se, após retornar do seu manipulador final, a exceção ainda não tiver sido tratada adequadamente, então o manipulador final do sistema será acionado. Opcionalmente ele mostrará a caixa de mensagem que informa o fechamento do sistema. Dependendo da configuração do registro (registry), esta caixa de diálogo pode oferecer a opção de associar um debugger ao programa. Se não existir esta opção ou se o debugger não puder ser acionado, o programa é condenado e o sistema chamará ExitProcess para terminar o programa. 7. Entretanto, antes de terminar o programa, o sistema efetuará um "alinhamento final" da pilha para o thread no qual a exceção ocorreu. Se tudo o que foi dito é uma grande novidade para você, não se preocupe. Com o tempo e um pouco de prática a coisa fica bem mais fácil. Só não é possível escapar desta teoria toda... coisas da vida.
As vantagens de se usar Assembly no tratamento de exceções O win32 tem apenas umas poucas funções na API para o tratamento de erros. Isto nos força a escrever a maior parte do código para um manipulador de exceções. Os programadores de "C" podem lançar mão de várias facilidades oferecidas pelos compiladores, incluindo no seu código fonte declarações como _try, _except, _finally, _catch e _throw. Uma desvantagem importante em depender do código gerado por compiladores "C" é que o tamanho do executável final pode ser aumentado, e muito! Além disto, a maioria dos programadores em "C" não tem idéia do tipo de código que é produzido pelo compilador para manipular as exceções e, para fazer um tratamento de erros eficaz, é preciso ter flexibilidade, saber o que se está fazendo e ter um controle absoluto do processo. Isto porque as exceções podem ser interceptadas e tratadas de várias maneiras e em vários níveis diferentes do código. Usando Assembly, você poderá produzir um código enxuto, confiável, flexível e que atenda especificamente o seu aplicativo. Aplicativos multi-threaded necessitam de um tratamento ainda mais cuidadoso e a linguagem Assembly oferece um modo simples e versátil de adicionar manipuladores de exceção a programas deste tipo. Obter informações a respeito do tratamento de exceções em baixo nível não é nada fácil. Os exemplos do SDK (Software Development Kit) do win32, ao invés de explorarem o uso da sua estrutura básica, mostram somente como utilizar declarações do compilador "C". As informações para este texto foram obtidas usando um programa teste, de um debugger e desassemblando código produzido por compiladores "C". O programa except.exe (que será criado neste tutorial) demonstra as técnicas descritas a seguir.
Os dois tipos de manipuladores de exceção Com certeza você vai se surpreender com que facilidade é possível associar manipuladores de exceção aos seus programas. Existem dois tipos de manipuladores de exceção: os thread-específicos e os finais.
Os manipuladores finais O manipulador de exceção "final" é chamado pelo sistema quando o seu programa estiver "marcado para morrer". Este manipulador é específico do processo e não está vinculado ao thread que causou a exceção. Este tipo de manipulador geralmente é inserido no thread principal, logo depois do ponto de entrada do programa, através de uma chamada à função da API denominada SetUnhandledExceptionFilter , cuja tradução literal é "configure um filtro de exceções não tratadas". Colocado neste nível, o manipulador cobre o programa a partir deste ponto até o fim do mesmo. Não há a necessidade de remover este manipulador quando o programa termina - o windows faz isto automaticamente. Por exemplo: ; ponto de entrada do programa inicio: push OFFSET manip_final call SetUnhandledExceptionFilter ... ; código coberto pelo manipulador final ... ... call ExitProcess ; --------------------------------------------------------
manip_final: ... ... ... mov eax, -1 ret
; código para a "gentil" mensagem de despedida ; (eax=-1 Restaura contexto e continua)
Existe um (e apenas um) manipulador final ativo. Se a função SetUnhandledExceptionFilter for chamada uma segunda vez, o endereço do manipulador final é alterado para o novo valor e a versão anterior é descartada.
Os manipuladores thread-específicos Este tipo de manipulador é usado para vigiar áreas específicas de código. É acionado alterando-se o valor mantido pelo sistema em FS:[0]. Cada thread do programa tem um valor diferente no registrador de segmento FS, de modo que o manipulador é sempre específico para cada thread. O manipulador será acionado se ocorrer uma exceção durante a execução do código protegido pelo manipulador.
O valor em FS é um seletor de 16 bits que aponta para o "Thread Information Block", uma estrutura que contém as informações de cada thread. O primeiro dword do Thread Information Block aponta para uma estrutura que passaremos a chamar de estrutura "ERR". Esta estrutura "ERR" possui no mínimo 2 dwords:
1° dword + 0
Ponteiro para a próxima estrutura "ERR"
2° dword + 4 Ponteiro para o manipulador de exceção particular De posse destas informações, criar um manipulador de exceções thread-específico é muito fácil. Exemplo: push OFFSET manipulador ; endereço da próxima estrutura "ERR" push FS:[0] mov FS:[0], esp ; passar para FS:[0] o endereço de "ERR" ... ... ; Código coberto pelo manipulador ... pop FS:[0] ; restaurar o endereço de "ERR" em FS:[0] ; descartar o resto da estrutura "ERR" add esp, 4h ; -------------------------------------------------------------------manipulador: ... ... ... mov eax, 1 ret
; código do manipulador de exceção ; eax=1 vai para o próximo manipulador ; eax=0 restaura contexto e continua execução
Encadeamento de manipuladores de exceção thread-específicos: no código acima podese observar que o 2° dword da estrutura ERR, que é o endereço do manipulador, é colocado na pilha em primeiro lugar. Depois o 1° dword da estrutura ERR subsequente é colocado na pilha através da instrução PUSH FS:[0]. Suponha que o código protegido por este manipulador tenha chamado outras funções que necessitem ter suas próprias proteções individuais. Neste caso, você pode criar outra estrutura ERR com um manipulador para proteger este código exatamente da mesma maneira. Isto é denominado encadeamento (chaining). Na prática isto significa que, quando ocorrer uma exceção, o sistema irá percorrer a cadeia de manipuladores chamando inicialmente o manipulador de exceção mais atual, aquele estabelecido logo antes do código onde a exceção ocorreu. Se este manipulador não lidar com a exceção (retornando EAX=1), então o sistema chama o anterior da cadeia. Como cada estrutura ERR contém o endereço do manipulador anterior, pode-se estabelecer qualquer quantidade de manipuladores deste tipo. Cada manipulador poderá proteger ou lidar com tipos particulares de exceção, dependendo do código que você lhes atribuir. A pilha é usada para manter as estruturas ERR, evitando que sejam sobre-escritas. Entretanto, nada impede o uso de outras partes da memória para guardar estruturas ERR - depende do gosto de cada um.
Desdobramento da pilha (stack unwind) Agora vamos dar uma olhada no chamado "stack unwind", que pode ser traduzido como
"desdobramento da pilha", para acabar com este "mistério". Um "desdobramento da pilha" soa um tanto dramático mas, na prática, consiste em simplesmente chamar os manipuladores de exceção cujos dados locais estejam localizados mais abaixo na pilha e depois (provavelmente) continuar a execução a partir de uma outra moldura (frame). Em outras palavras, o programa é preparado para ignorar o conteúdo da pilha entre estas duas posições. Caso você não saiba o que é uma moldura de pilha, revise o conceito lendo o texto Tratamento de erros. Uso da pilha pela função C ... 3ª Moldura da Pilha
Manipulador de Exceções C Dados Locais da Função C Endereço de retorno da Função C
2ª Moldura da Pilha
Uso da pilha pela função B ... Manipulador de Exceções B Dados Locais da Função B Endereço de retorno da Função B
1ª Moldura da Pilha
Uso da pilha pela função A ... Manipulador de Exceções A Dados Locais da Função A Endereço de retorno da Função A ...
Neste caso, quando cada uma das funções é chamada, são PUSHadas coisas na pilha: em primeiro lugar o endereço de retorno, depois os dados locais e finalmente o manipulador de exceções (esta é a estrutura "ERR" mencionada anteriormente). Agora suponha que tenha ocorrido uma exceção na Função C. Como vimos, o sistema iniciará uma caminhada pela cadeia de manipuladores. O manipulador 3 será o primeiro a ser chamado. Imagine que o manipulador 3 não trate a exceção (retornando EAX=1), então o manipulador 2 será chamado. Se o manipulador 2 também retornar EAX=1, então o manipulador 1 será chamado. Se o manipulador 1 tratar a exceção, ele precisará "desarmar" os dados locais nas molduras da pilha criadas pelas Funções B e C. Isto é feito através do Desdobramento. O desdobramento simplesmente repete a caminhada na cadeia de manipuladores chamando inicialmente o manipulador 3, depois o 2 e finalmente o 1. As diferenças entre este tipo de caminhada pela cadeia de manipuladores e a caminhada iniciada pelo sistema quando a exceção ocorreu pela primeira vez são as seguintes: 1. A caminhada é iniciada pelo manipulador e não pelo sistema. 2. A flag de exceção no registro EXCEPTION_RECORD deveria receber o valor 2h (EH_UNWINDING). Este valor indica ao manipulador thread específico que
ele está sendo chamado por outro manipulador situado mais adiante na cadeia e que deve desarmá-lo usando dados locais. Não deve fazer nada além disso e precisa retornar EAX=1. 3. A caminhada termina imediatamente antes do chamador. No exemplo do diagrama, se o manipulador 1 iniciar a caminhada, o último manipulador a ser chamado durante o desdobramento será o manipulador 2. Não existe a necessidade do manipulador 1 ser chamado por ele mesmo porque ele tem acesso aos seus próprios dados locais para desarmar-se.
Como é feito o desdobramento O manipulador pode iniciar um desdobramento usando a função da API RtlUnwind ou, como veremos adiante, usando o código que você escrever. Esta função pode ser chamada da seguinte forma: PUSH PUSH PUSH PUSH CALL
Valor de Retorno pRegistroDeExceção ADDR MarcadorDoCodigo UltimaMolduraDaPilha RtlUnwind
Valor de Retorno contém um valor de retorno depois do desdobramento, o qual,
provavelmente, nem será usado.
pRegistroDeExceção é um ponteiro para o registro de exceção, o qual é uma das
estruturas enviadas ao manipulador responsável pela área onde ocorreu a exeção. MarcadorDoCodigo é o local a partir do qual a execução deve continuar depois do
desdobramento e, tipicamente, é o endereço do código imediatamente após a chamada a RtlUnwind . Se não for especificado, a função da API funciona normalmente, porém é melhor não brincar com este tipo de função e garantir que funcione adequadamente. UltimaMolduraDaPilha é a moldura da pilha na qual o desdobramento deve parar.
Normalmente é o endereço da pilha da estrutura ERR que contém o endereço do manipulador que iniciou o desdobramento.
Observação: Diferentemente de outras funções da API, não deixe para RtlUnwind preservar os registradores EBX, ESI ou EDI – se você for usar esta função, o correto é preservá-los fazendo um PUSH antes do primeiro parâmetro e restaurá-los com POP após MarcadorDoCodigo.
Código próprio de Desdobramento O código a seguir simula o desdobramento (onde ebx guarda o endereço da estrutura EXCEPTION_RECORD enviada ao manipulador): MOV D[EBX+4],2h FS MOV EDI,[0]
; faz a flag de exceção EH_UNWINDING ; pega o endereço do 1° manipulador thread específico
L2: CMP D[EDI],-1 ; vê se é o último JZ >L3 ; sim, então termina PUSH EDI,EBX ; push estrutura ERR, EXCEPTION_RECORD CALL [EDI+4] ; chama manipulador para desarme ADD
Extended Stack Pointer
Ponteiro de Pilha Estendido - um registrador que guarda o endereço do topo da pilha.
', CAPTION, 'ESP',BELOW,RIGHT, WIDTH, 300, FGCOLOR, '#CCCCFF', BGCOLOR, '#333399', TEXTCOLOR, '#000000', CAPCOLOR, '#FFFFFF', OFFSETX, 10, OFFSETY, 10);" onmouseout="return nd();" > ESP,8h ; remove os dois parâmetros PUSHados ; pega ponteiro para a próxima estrutura ERR MOV EDI,[EDI] ; e processa o próximo se não tiver terminado JMP L2 L3: ; marcador do código quando terminar
Neste caso cada manipulador é chamado com a flag de exceção 2h até que o último manipulador seja alcançado (o sistema possui o valor -1 na última estrutura ERR). O código acima não checa valores corrompidos em [EDI] e em [EDI+4]. O primeiro é um endereço da pilha e poderia ser checado verificando se está acima da base da pilha do thread indicada em FS:[8] e abaixo do topo da pilha do thread indicada em FS:[4]. O segundo é um endereço do código de modo que é possível checar se está situado entre dois marcadores de código, um no começo do seu código e outro no fim do mesmo. Alternativamente é possível checar se [EDI] e [EDI+4] podem ser lidos chamando a função da API IsBadReadPtr .
Desdobrar pelo manipulador final e depois continuar Não é apenas um manipulador thread específico que pode iniciar um desdobramento de pilha. O desdobramento também pode ser realizado pelo manipulador final chamando RtlUnwind ou através de um código próprio que faça o desdobramento retornando posteriormente EAX=-1. (Veja "Continuar a execução depois de chamar o manipulador final").
Desdobramento final e depois terminar Se um manipulador final estiver instalado e ele retornar EAX=0 ou EAX=1, o sistema fará com que o processo termine. Entretanto, antes do término acontece uma coisa interessante. O sistema faz um desdobramento final voltando ao primeiro manipulador da cadeia (ou seja, o manipulador que contém o código no qual ocorreu a exeção). Esta é a última oportunidade que seu manipulador tem de executar o código de desarme necessário em cada moldura da pilha. Você pode ver claramente este desdobramento final ocorrendo se configurar o programa demo Except.exe para permitir que a exceção vá até o manipulador final e pressionar F3 ou F5 quando alcançar este ponto. O mesmo acontece com o programa mais simples, Except1.exe (os dois programas estão disponíveis na seção de downloads da Aldeia em Tutoriais / Assembly Numaboa ou no final deste artigo).
A informação enviada aos manipuladores A informação enviada aos manipuladores (handlers) precisa ser suficientemente clara para que estes possam tentar corrigir a exceção, fazer logs de erros ou avisar o usuário. Como veremos adiante, esta informação, quando os manipuladores são chamados, é enviada pelo próprio sistema através da pilha. Adicionalmente você pode enviar suas próprias informações para os manipuladores aumentando a estrutura ERR para que possa conter mais informações.
Informação enviada ao manipulador final O manipulador final está documentado no Windows Software Development Kit (SDK) como API "UnhandledExceptionFilter". Ele recebe apenas um parâmetro, um ponteiro para a estrutura EXCEPTION_POINTERS. Esta estrutura é a seguinte:
EXCEPTION_POINTERS +0
Ponteiro para a próxima estrutura EXCEPTION_RECORD
+4
Ponteiro para a estrutura do registro CONTEXT
A estrutura EXCEPTION_RECORD contém os seguintes campos:
EXCEPTION_RECORD +0
Código de exceção (ExceptionCode)
+4
Flag de exceção (ExceptionFlag)
+8
Registro de exceção aninhado (NestedExceptionRecord)
+C
Endereço da exceção (ExceptionAddress)
+10
Parâmetros numéricos (NumberParameters)
+14
Dados adicionais (AdditionalData)
onde 1. ExceptionCode dá o tipo de exceção que ocorreu. Existem muitos deles listados nos arquivos SDK e de cabeçalho (header) mas, na prática, os tipos que você geralmente encontra são: o C0000005h – Violação de escrita ou leitura da memória o C0000094h – Divisão por zero o C0000095h – Overflow de divisão o C00000FDh – A pilha ultrapassou o tamanho máximo disponível o 80000001h – Violação de uma página guard na memória gerada por Virtual Alloc
O seguinte apenas ocorre quando se lida com exceções:
C0000025h – Uma exceção não-continuável – o manipulador não deve tentar tratá-la o C0000026h – Código de exceção usado pelo sistema durante o tratamento de exceções. Este código pode ser utilizado se o sistema encontrar um retorno inesperado de um manipulador. Também é usado se um Registro de Exceção (ExceptionRecord) não for fornecido numa chamada RtlUnwind. Para debugar são usados os seguintes: o 80000003h – Ocorreu uma parada (breakpoint) porque foi encontrado um INT3 no código. o 80000004h – Passo único (single step) durante o debugging. o
Os códigos de exceção seguem as seguintes regras:
Bits 31-30
Bit 29
Bit 28
Bits 27-0
0=sucesso 1=Informação 0=Microsoft Reservado Código 2=Alerta 1=Aplicação Precisa ser zero 3=erro Um código de erro personalizado típico enviado por RaiseException poderia ser, por exemplo, E0000100h (erro, applicação, código=100h). Se necessário, este é um modo rápido de sair do código e ir direto ao seu próprio manipulador. 2. A ExceptionFlag dá instruções ao manipulador. Os valores podem ser: o 0 - uma exceção continuável (pode ser reparada). o 1 - exceção não-continuável (não pode ser reparada). o 2 - a pilha está se desdobrando - não tente reparar. 3. O NestedExceptionRecord aponta para um outra estrutura EXCEPTION_RECORD se o próprio manipulador tenha causado uma nova exceção. 4. ExceptionAddress é o endereço no código onde ocorreu a exceção. 5. NumberParameters é o número de dwords que devem ser seguidos na AdditionalInformation. 6. AdditionalInformation é um array de dwords com mais informações. Tanto pode ser informação enviada pela aplicação quando chamou RaiseException ou, se o código de exceção for C0000005h, será o seguinte: 1º dword - 0=violação de leitura, 1=violação de escrita; 2º dword - endereço da violação de acesso. A segunda parte da estrutura EXCEPTION_POINTERS que é enviada ao manipulador final aponta para a estrutura do registro CONTEXT, a qual contém os valores de todos os registros, específicos do processador, no momento em que ocorreu a exceção. WINNT.H contém as estruturas CONTEXT para vários processadores. Seu programa pode determinar o tipo de processador que está em uso chamando GetSystemInfo. A estrutura CONTEXT é a seguinte para o IA32 (Intel 386 e posteriores):
+0
(usado quando se chama GetThreadContext)
REGISTRADORES DE DEBUG
+4
registrador debug número 0
+8
registrador debug número 1
+C
registrador debug número 2
+10
registrador debug número 3
+14
registrador debug número 6
+18
registrador debug número 7
PONTO FLUTUANTE / REGISTRADORES MMX +1C
ControlWord
+20
StatusWord
+24
TagWord
+28
ErrorOffset
+2C
ErrorSelector
+30
DataOffset
+34
DataSelector
+38
Registradores FP x 8 (10 bytes cada)
+88
CrONpxState
REGISTRADORES DE SEGMENTO +8C
Registrador gs
+90
Registrador fs
+94
Registrador es
+98
Registrador ds
REGISTRADORES COMUNS +9C
Registrador edi
+A0
Registrador esi
+A4
Registrador ebx
+A8
Registrador edx
+AC
Registrador ecx
+B0
Registrador eax
REGISTRADORES DE CONTROLE +B4
Registrador ebp
+B8
Registrador eip
+BC
Registrador cs
+C0
Registrador eflags
+C4
Registrador esp
+C8
Registrador ss
Informação enviada ao manipulador thread-específico Quando o manipulador thread-específico é chamado, ESP aponta para as seguintes três estruturas:
ESP +4 Ponteiro para a estrutura EXCEPTION_RECORD ESP +8
Ponteiro para uma estrutura ERR própria
ESP +C
Ponteiro para a estrutura registro CONTEXT
Diferentemente de outros CALLBACKs no Windows, quando um manipulador threadespecífico é chamado, é usada a convenção de chamada C (o chamador precisa remover os argumentos da pilha) e não a convenção de chamada PASCAL (a função remove os argumentos da pilha). Pode-se verificar esta peculiaridade no código do Kernel32 usado para fazer a chamada: PUSH Param, CONTEXT record, ERR, EXCEPTION_RECORD CALL HANDLER ADD ESP,10h
Na prática, o primeiro argumento, Param, parece não conter informações relevantes. As estruturas EXCEPTION_RECORD e registro CONTEXT já foram descritas acima. A estrutura ERR é a estrutura que você criou na pilha quando o manipulador foi constituído e precisa conter o ponteiro para a próxima estrutura ERR, além do endereço do código do manipulador que está sendo instalado (reveja como criar um manipulador no tópico "Tipos de Manipuladores"). O ponteiro para a estrutura ERR passada ao manipulador thread-específico está no topodesta estrutura. Portanto, é possível aumentar a estrutura ERR de modo que o manipulador possa receber informações adicionais. Num arranjo típico, a estrutura ERR poderia ser a seguinte, onde [ESP=8h] aponta para o topo desta estrutura quando o manipulador é chamado:
ERR +0
Ponteiro para a próxima estrutura ERR
ERR +4
Ponteiro para o manipulador de exceção próprio
ERR +8 Endereço no código de um "lugar seguro" para o manipulador ERR +C
Informação para o manipulador
ERR +10
Área para as flags
ERR +14
Valor de EBP no "lugar seguro"
Como veremos adiante ("Seguindo a execução a partir de um lugar seguro"), os campos
+8 e +14 podem ser utilizados pelo manipulador para fazer a recuperação desta exceção.
Fornecendo acesso a dados locais Agora vamos analisar a melhor posição para a estrutura ERR na pilha, relativa à moldura da pilha, e que pode muito bem conter variáveis de dados locais. Isto é importante por que o manipulador eventualmente precisa acessar estes dados locais para poder realizar a "limpeza" convenientemente. Aqui está um código típico que pode ser usado para criar um manipulador thread-específico onde haja dados locais: MINHAFUNCAO: PUSH EBP
; ; ; MOV EBP,ESP ; ; ; SUB ESP,40h ; ;******** dados locais agora em ;********** instala manipulador ; PUSH EBP ; ; PUSH 0 ; PUSH 0 PUSH ADDR LUGAR_SEGURO ; ; PUSH ADDR HANDLER FS PUSH [0] ; ; FS MOV [0],ESP ... ; ; ... ... ; ; JMP >L10 ; ; LUGAR_SEGURO: ; ; L10: FS POP [0] ; MOV ESP,EBP POP EBP RET ;***************** HANDLER: RET
ponto de entrada do procedimento salva ebp (usado para endereçar o quadro da pilha usa EBP como ponteiro para o quadro da pilha faz 16 dwords na pilha para dados locais [EBP-4] a [EBP-40h] e sua estrutura ERR ERR+14h salva ebp (ebp em lugar seguro) ERR+10h área para flags ERR+0Ch informação para o manipulador ERR+8h novo eip em lugar seguro ERR+4h endereço do manipulador ERR+0h manter próximo ERR na cadeia apontar para ERR recém-criado na pilha
código protegido vem aqui fim normal se não ocorrer nenhuma exceção manipulador ajusta eip/esp/ebp para aqui restaura próxima ERR na cadeia
Usando este código, quando o manipulador é chamado, [ESP+8h] está apontando para o topo da estrutura ERR (isto é, ERR+0h) e o seguinte encontra-se na pilha:
.
pilha +ve
ERR +0
Ponteiro para a próxima estrutura ERR
ERR +4
Ponteiro para o manipulador de exceção próprio
ERR +8 Endereço no código de "lugar seguro" para o manipulador ERR +C
Informação para o manipulador
ERR +10
Área para as flags
ERR +14
Valor de EBP no "lugar seguro"
ERR +18
Dados locais
ERR +1C
Dados locais
ERR +20
Dados locais mais dados locais
Fica fácil perceber que, quando o manipulador recebe um ponteiro para a estrutura ERR, ele também pode achar o endereço dos dados locais na pilha. Isto é possível por que o manipulador conhece o tamanho da estrutura ERR e a posição dos dados locais na pilha. Se o campo EBP for usado em ERR+14h, como no exemplo acima, ele também poderia ser usado como um ponteiro para os dados locais.
Recuperando-se e reparando uma exceção Continuando a partir de um lugar seguro Você precisa continuar a execução num lugar do código que não cause mais problemas. A coisa mais importante que deve ser observada é que, se o seu programa foi escrito para trabalhar dentro do framework do Windows, seu objetivo é o de retornar ao sistema o mais rápido possível e de forma controlada de modo que se possa esperar pelo próximo evento do sistema. Se a exceção ocorreu durante uma chamada do sistema a um procedimento de uma janela, então um bom lugar seguro será próximo do ponto de saída do procedimento da janela para que o sistema retome o controle de maneira "limpa". Desta forma, o sistema terá a impressão de que sua aplicação retornou do procedimento da janela da forma usual. Entretanto, se a exceção ocorrer num trecho de código onde não existe um procedimento de janela, então será preciso exercer um controle maior. Por exemplo, um thread estabelecido para determinadas tarefas provavelmente precisará ser terminado, avisando o thread principal que a tarefa não pode ser completada. Outra consideração importante é a facilidade de colocar os valores corretos de EIP, ESP e EBP no lugar seguro. Veremos a seguir que isto não é nada complicado. São tantas as possibilidades que podem ser exploradas que seria inútil tentar mencionálas todas. O lugar seguro exato depende da natureza do seu código e do uso que você está fazendo da manipulação de exceções. Entretanto, dê novamente uma olhada no código acima referente a MINHAFUNCAO. Você pode ver o marcador de código "LUGAR_SEGURO". Isto é um endereço no código a partir do qual a execução poderia continuar com segurança com o manipulador tendo feito toda a "faxina" necessária.
No exemplo de código, para que a execução continue com sucesso, é preciso lembrar que, apesar de LUGAR_SEGURO estar dentro da mesma moldura de pilha da exceção ocorrida, os valores de ESP e EBP precisam ser cuidadosamente estabelecidos pelo manipulador antes que a execução continue a partir de EIP. Portanto, estes três registradores precisam ser estabelecidos pelas seguintes razões:
ESP - para que a instrução FS POP [0] esteja habilitada a trabalhar e, se necessário, para POP de outros valores. EBP - para assegurar que os dados locais possam ser endereçados dentro do manipulador e para restaurar o valor de retorno correto de MINHAFUNCAO em ESP. EIP - para forçar que a execução continue a partir de LUGAR_SEGURO.
Agora é possível perceber que cada um destes valores pode ser rapidamente obtido dentro da função de manipulação. O valor correto de ESP é, de fato, exatamente o mesmo que o do topo da própria estrutura ERR (dado por [ESP+8h] quando o manipulador é chamado). O valor correto de EBP pode ser obtido de ERR+14h porque foi PUSHado para a pilha quando a estrutura ERR foi montada. E o endereço correto do código do LUGAR_SEGURO que deve ser passado para EIP está em ERR+8h. Neste ponto estamos prontos para observar, caso ocorra um erro, como o manipulador pode garantir que a execução continue a partir do lugar seguro ao invés de permitir que o processo termine. HANDLER: PUSH EBP MOV EBP,ESP ;** agora [EBP+8]=ponteiro para EXCEPTION_RECORD ;** [EBP+0Ch]=ponteiro da estrutura ERR para o registro CONTEXT ;** [EBP+10h]=ponteiro; salva registradores requeridos pelo ; Windows para ter o registro da exceção em ebx PUSH EBX,EDI,ESI MOV EBX,[EBP+8] TEST D[EBX+4],1h ; ve se é uma exceção não-continuável ; sim, precisa ser tratada JNZ >L5 TEST D[EBX+4],2h ; verifica se é EH_UNWINDING ; não JZ >L2 ... ; limpa código enquanto faz desdobramento ... ... ; precisa retornar 1 para ir ao próximo JMP >L5 ; manipulador L2: PUSH 0 ; valor de retorno (não usado) ; ponteiro para este registro de exceção PUSH [EBP+8h] PUSH ADDR UN23 ; endereço do código para RtlUnwind retornar ; ponteiro para esta estrutura ERR PUSH [EBP+0Ch] CALL RtlUnwind UN23: MOV ESI,[EBP+10h] ; obtém registro de contexto em esi ; obtém ponteiro para a estrutura ERR MOV EDX,[EBP+0Ch] ; usa como novo esp MOV [ESI+0C4h],EDX
MOV MOV MOV MOV XOR
EAX,[EDX+8]
[ESI+0B8h],EAX EAX,[EDX+14h] [ESI+0B4h],EAX EAX,EAX
JMP >L6 L5: MOV EAX,1 L6: POP ESI,EDI,EBX MOV ESP,EBP POP EBP RET
; ; ; ; ; ;
obtém lugar seguro dado na estrutura ERR insere novo eip obtém ebp no lugar seguro de ERR insere novo ebp recarrega contexto e retorna eax=0 ao sistema
; vai para o próximo manipulador ; retorna eax=1 ; retorno normal (sem argumentos)
Reparando a exceção No exemplo acima você viu que o contexto carregado com o novo EIP, EBP e ESP faz com que a execução continue a partir de um lugar seguro. Também é possível, usando o mesmo método de substituir os valores de alguns registradores do contexto, "consertar" a exceção e permitir que a execução continue a partir de um local próximo do código faltoso para que a atual tarefa possa ser realizada. Um exemplo típico seria a divisão por zero, que pode ser reparada por um manipulador que substitua o valor do divisor por 1 e depois retorne EAX=0 (se for um manipulador thread-específico) fazendo com que o sistema recarregue o contexto e continue a execução. No caso de violações de memória, você pode utilizar o fato de que o endereço da violação de memória é passado como o segundo dword no campo additional information do registro da exceção. O manipulador pode usar este valor, passá-lo para VirtualAlloc e abrir mais memória a partir deste ponto. Se obtiver sucesso, o manipulador pode então recarregar o contexto (não modificado) e retornar EAX=0 para continuar a execução (caso seja um manipulador thread-específico).
Continuando a execução após a chamada de um manipulador final Se quiser, você pode lidar com exceções no manipulador final. Você ainda deve se lembrar que o manipulador final é chamado pelo sistema quando o processo está prestes a terminar. Os retornos do manipulador final em EAX não são os mesmos dos manipuladores thread-específicos. Se o retorno for EAX=0, o processo termina sem a caixa de mensagem; se o retorno for EAX=1, a caixa de mensagem é mostrada. Existe um terceiro código de retorno, o EAX=-1. Este é descrito no SDK como "EXCEPTION_CONTINUE_EXECUTION". Este retorno tem o mesmo efeito do
EAX=0 de um manipulador thread-específico, isto é, recarrega o registro de contexto no processador e continua a execução a partir do EIP do contexto. É claro que o manipulador final pode alterar o registro de contexto antes de retornar ao sistema, da mesma forma que um manipulador thread-específico. Deste modo, um manipulador final pode recuperar uma exceção continuando a execução a partir de um lugar seguro apropriado ou pode tentar reparar a exceção. Apesar disto, perde-se alguma flexibilidade. Em primeiro lugar, não é possível aninhar manipuladores finais. Só é possível ter um manipulador final ativo estabelecido por SetUnhandledExceptionFilter . Você pode, se quiser, mudar o endereço do manipulador final à medida que diferentes porções do seu código sejam processadas. SetUnhandledExceptionFilter retorna o endereço do manipulador final que está sendo substituído de modo que seria possível fazer o seguinte: PUSH CALL PUSH ... ... ... CALL
ADDR FINAL_HANDLER SetUnhandledExceptionFilter ; guarda endereço do manipulador anterior
EAX
; este é o código sendo guardado
SetUnhandledExceptionFilter
; restaura manipulador anterior
Observe que, no momento da segunda chamada a SetUnhandledExceptionFilter, o endereço do manipulador anterior já está na pilha devido à instrução PUSH EAX. Outra dificuldade em usar o manipulador final é que a informação que lhe é enviada limita-se ao registro de exceção e ao registro de contexto. Portanto, será preciso manter na memória estática o endereço do código do lugar seguro, além dos valores de ESP e EBP do lugar seguro. Isto pode ser facilmente implementado em tempo de execução. Por exemplo, quando se usa a mensagem WM_COMMAND dentro de um procedimento de janela PROCESS_COMMAND: MOV EBPLUGAR_SEGURO,EBP MOV ESPLUGAR_SEGURO,ESP ... ... ... LUGAR_SEGURO: XOR EAX,EAX RET
; chamado em uMsg=111h (WM_COMMAND) ; mantém ebp num lugar seguro ; mantém esp num lugar seguro ; código protegido aqui ; marcador para o lugar seguro ; retorna eax=0=mensagem processada
No exemplo acima, para reparar a exceção a partir de um lugar seguro, o manipulador iria inserir os valores de EBPLUGAR_SEGURO em CONTEXT+0B4h (ebp), ESPLUGAR_SEGURO em CONTEXT+0C4h (esp), ADDR LUGAR_SEGURO em CONTEXT+0B8h (eip) e depois retornar -1. Note que, num desdobramento de pilha forçado pelo sistema devido a uma saída fatal, apenas são chamados os manipuladores "thread-específicos" (se houver algum) e não o manipulador final. Se não existirem manipuladores "thread-específicos", o manipulador final terá que administrar toda a "limpeza" antes de retornar ao sistema.
Passo-a-passo com uma flag no manipulador Você pode fazer um testador passo-a-passo para o seu programa durante o desenvolvimento usando a propriedade dos manipuladores de poder armar uma flag no contexto dos registradores antes de retornar ao sistema. Você pode determinar que o manipulador mostre os resultados na tela ou então que os armazene num arquivo. Isto pode ser útil se você suspeitar que os resultados estejam sendo alterados durante o processo de debug ou então se você precisar observar rapidamente como um detrminado trecho de código responde a várias entradas. Insira o seguinte fragmento de código onde o passo-a-passo deve começar: MOV D[SSCOUNT],5 INT 3
SSCOUNT é um símbolo de dados e contém o número de passos que o manipulador deve dar antes de retornar à sua operação normal. A INT3 provoca uma exceção 80000003h assim que seu manipulador for chamado. O código no seu programa em desenvolvimento deveria ser protegido por um manipulador thread-específico por um código do tipo: SS_HANDLER: PUSH EBP MOV EBP,ESP PUSH EBX,EDI,ESI MOV EBX,[EBP+8] TEST D[EBX+4],01h JNZ >L14 TEST D[EBX+4],02h JNZ >L14 MOV ESI,[EBP+10h] MOV EAX,[EBX] CMP EAX,80000004h
; ; ; ; ; ; ; ; ; ; ; ; ;
como exigido pelo Windows, salva os registradores pega registro de exceção em ebx vê se é uma exceção não-continuável sim vê se EH_UNWINDING sim pega registro do contexto em esi pega ExceptionCode vê se está aqui porque flag está armada sim vê se é INT3 para o passo-a-passo não
JZ >L10 CMP EAX,80000003h JNZ >L14 L10: ;
Não existe mais diferença entre pára (do verbo parar) e para (preposição). Use PARA nos dois casos.
', CAPTION, 'pára',BELOW,RIGHT, WIDTH, 300, FGCOLOR, '#CCCCFF', BGCOLOR, '#333399', TEXTCOLOR, '#000000', CAPCOLOR, '#FFFFFF', OFFSETX, 10, OFFSETY, 10);" onmouseout="return nd();" > pára se atingiu o número de passos JZ >L12 ; arma a flag no contexto OR D[ESI+0C0h],100h L12: ... ... ; código para mostrar na tela
... XOR EAX,EAX JMP >L17 L14: MOV EAX,1
; eax=0 recarrega contexto e retorna ; ao sistema
; eax=1 sistema vai para o próximo ; manipulador
L17: POP ESI,EDI,EBX MOV ESP,EBP POP EBP RET
Aqui, a primeira chamada ao manipulador é causada pela INT3 (o sistema estrilou um bocado quando tentei usar INT1). Recebendo esta exceção, que poderia ter vindo apenas do fragmento de código inserido no código-a-ser-testado, o manipulador arma a flag no contexto antes de retornar. Isto faz com que uma exceção 80000004h alcance o manipulador na próxima instrução. Note que, com estas exceções, eip já está na próxima instrução, isto é, uma além da INT3 ou além da instrução executada com a flag armada. Tudo o que é preciso fazer no manipulador para continuar o passo-a-passo é armar a flag novamente e retornar ao sistema. (Agradeço ao G.W.Wilhelm, Jr da IBM pela idéia)
Tratamento de exceções em aplicações multi-thread Quando se trata de aplicações multi-thread, o sistema oferece pouca ou nenhuma ajuda no tratamento das exceções. Você vai ter que planejar proteções para falhas prováveis e organizar seus threads de acordo. As regras que se aplicam ao tratamento de exceções pelo sistema, no contexto de aplicações multi-thread, são: 1. Apenas um tipo 1 (manipulador final) pode existir a qualquer tempo para cada processo. Se um novo manipulador chamar SetUnhandledExceptionFilter , isto simplesmente substituirá o manipulador final - não existe uma cadeia de manipuladores finais como há para os manipuladores tipo 2 (thread-específicos). Portanto, o uso mais simples de um manipulador final provavelmente ainda é o melhor numa aplicação multi-thread - estabelecê-lo no thread principal o mais cedo possível após o ponto de entrada do programa. 2. O manipulador final será chamado pelo sistema se o processo está para ser terminado, independentemente de qual thread tenha causado a exceção. 3. Entretanto, um desdobramento final (imediatamente antes do término) só vai ocorrer nos manipuladores thread-específicos estabelecidos para o thread que tenha causado a exceção. Mesmo que outros threads (inocentes) possuam uma janela e um loop de mensagem, o sistema não irá avisá-los de que o processo está para ser encerrado (nenhuma mensagem especial lhes será enviada além das mensagens usuais provenientes da perda de foco de outras janelas). 4. Portanto, os outros manipulaodres (inocentes) não podem esperar um desdobramento final se o processo estiver para terminar. Continuarão sem saber que o término é iminente.
5. Se, o que é provável, estes outros threads inocentes precisarem ser desarmados, você precisará informá-los a partir do manipulador final. O manipulador final precisará esperar até que estes outros threads tenham completado a "limpeza" para poder retornar ao sistema. 6. O modo como os threads inocentes são informados sobre o término iminente do programa depende da acuidade do seu código. Se o thread inocente possuir uma janela e um loop de mensagem, então o manipulador final pode usar SendMessage para esta janela para enviar uma mensagem definida pela aplicação (precisa ser 400h ou acima) para informar este thread de que deve terminar com elegância. Se não existir uma janela e um loop de mensagem, o manipulador final poderia armar uma variável flag pública, rastreada de tempos em tempos pelo outro thread. Alternativamente, você poderia usar SetThreadContext para forçar o thread a executar determinado código de encerramento fazendo com que eip aponte para este código. Este método não funcionará se o thread estiver numa API, por exemplo, esperando o retorno de uma GetMessage. Neste caso, você também precisará enviar uma mensagem para garantir que o thread retorne da API e que o novo contexto seja configurado. 7. RaiseException só funciona no thread chamador, de modo que não pode ser usado para fazer a comunicação entre threads fazendo com que um thread inocente execute seu próprio código manipulador. 8. Como é que o manipulador final sabe quando pode prosseguir após informar os outros threads de que o programa está para terminar? SendMessage não retorna enquanto o destinatário não tiver voltado do seu procedimento de janela e o manipulador final não pode esperar por este retorno. Alternativamente, ele poderia inscrever uma flag que esperasse por uma resposta de um outro thread que ele acabou de desobstruir (observe que você precisa chamar a API Sleep no loop de inscrição para evitar a sobrecarga do sistema). Ou, ainda melhor, o manipulador final poderia esperar até que o outro thread termine (isto pode ser feito usando a API WaitForSingleObject ou WaitForMultipleObjects se houver mais de um thread). Alternativamente poderiam ser usadas as APIs Event ou Semaphore. 9. Para um exemplo de como estes procedimentos poderiam funcionar na prática, imagine que um thread secundário tenha a função de reorganizar uma base de dados e depois escrevê-la em disco. Pode estar no meio da tarefa quando o thread principal causa uma exceção, entrando no seu manipulador final. Neste caso você poderia abortar a tarefa do thread secundário, forçando seu desdobramento e seu término elegante, deixando os dados originais no disco ou, alternativamente, você poderia permitir que ele terminasse a tarefa e depois avisasse o manipulador que terminou, de modo que o manipulador possa voltar ao sistema. Você teria que impedir que o thread secundário iniciasse novas tarefas assim que seu manipulador fosse chamado. Neste caso o manipulador precisa armar uma flag que será testada pelo thread secundário antes de iniciar qualquer tarefa ou então usar as APIs Event. 10. Se a comunicação entre threads é difícil, existe uma outra maneira de um thread acessar a pilha de outro thread e, deste modo, causar um desdobramento. Neste caso faz-se uso do fato de que, apesar de cada thread possuir sua própria pilha, a memória reservada para esta pilha está no espaço de endereços do próprio processo. Você pode confirmar este fato observando uma aplicação multi-thread usando um debugger. Quando você se movimentar entre threads, os valores de
ESP e EBP mudarão, mas eles estarão dentro do espaço de endereços do próprio processo. O valor de FS também será diferente dependendo do thread e apontará para o Thread Information Block de cada thread. Assim, se você seguir os seguintes passos, um thread pode acessar a pilha e causar o desdobramento de outro: a. Guarde numa variável estática o valor do registrador FS de cada novo thread que for criado. b. Retorne as variáveis estáticas para zero quando cada um dos threads for fechado. c. O manipulador que precisar desdobrar outros threads deveria analisar cada uma das variáveis estáticas e, para as com valor diferente de zero (ou seja, o thread está ativo na hora da exceção), os manipuladores devem ser chamados com a flag de exceção 2 (EH_UNWINDING) e uma flag de usuário, digamos 400h, para mostrar que o manipulador thread-específico está sendo chamado pelo seu manipulador final. Você não pode chamar um manipulador threadespecífico num thread diferente usando RtlUnwind (que é thread-específico), mas pode fazê-lo usando o seguinte código (onde ebx contém o endereço do EXCEPTION_RECORD): 11. MOV D[EBX+4],402h 12. L1: 13. PUSH ES 14. MOV AX,[FS_VALUE] desdobrado 15. MOV ES ,AX 16. ES MOV EDI,[0] específico 17. POP ES 18. L2: 19. CMP D[EDI],-1 20. JZ >L3 21. PUSH EDI,EBX 22. CALL [EDI+4] desobstrução 23. ADD ESP,8h 24. MOV EDI,[EDI] 25. JMP L2 26. L3: 27. ; agora volta para L1
; faz a flag de exceção EH_UNWINDING+400h
; pega o valor de FS do thread que deve ser
; pega o endereço do primeiro manipulador thread-
; ; ; ;
vê se é o último sim, então termina PUSH estrutura ERR, EXCEPTION_RECORD chama o manipulador para rodar o código de
; remove os dois parâmetros PUSHados ; pega o ponteiro para a próxima estrutura ERR ; e faz o próximo se não estiver no fim ; marcador para quando terminar com um novo FS_VALUE até que todos os threads
; tenham sido processados
Aqui você vê que o Thread Information Block de cada thread inocente é lido usando o registrador ES, o qual, temporariamente, recebe o valor do registrador FS do thread. Ao invés de usar FS para achar o Thread Information Block, você pode usar o seguinte código para obter um endereço linear de 32 bits. Neste código, LDT_ENTRY é uma estrutura de 2 dwords, AX contém o valor de 16 bits do seletor (FS_VALUE) que deve ser convertido e hThread é qualquer manipulador de thread válido:
AND EAX,0FFFFh PUSH ADDR LDT_ENTRY,EAX,[hThread] CALL GetThreadSelectorEntry ; vê se falhou OR EAX,EAX JZ >L300 ; sim, então retorne zero MOV EAX,ADDR LDT_ENTRY MOV DH,[EAX+7] ; pega base alta ; pega meio da base MOV DL,[EAX+4] SHL EDX,16D ; desloca para o topo de edx ; e pega a base baixa MOV DX,[EAX+2] OR EDX,EDX ; agora edx=endereço linear de 32 bits L300: ; retorna nz se sucesso
A razão da importância (usando a flag 400h) de informar o manipulador chamado de que está sendo chamado por outro thread (o manipulador final) é que o thread chamado ainda está rodando porque a exceção ocorreu num thread diferente. É claro que o manipulador, nesta circunstância, precisa suspender o thread para que a tarefa de desobstrução possa ser realizada pelo thread chamador. O thread inocente, então, recebe um lugar seguro para ir antes de chamar ResumeThread. Tudo isto precisa ser feito antes que o manipulador final receba permissão para voltar ao sistema porque, no retorno, o sistema simplesmente irá terminar todos os threads na força bruta.
O programa exept1 Este programa é um exemplo simples de como a manipulação de exceções pode ser usada na prática em programas para Windows escritos em Assembly. O código fonte está no arquivo Except1.asm, escrito com a sintaxe GoAsm. Apesar de ser um programa GDI Windows, baseia-se somente em caixas de mensagem, motivo pelo qual não existem loops de mensagem. O programa possui dois manipuladores de exceção, um final e um thread-específico. O manipulador final é criado primeiro, depois é chamado um procedimento, cujo código está protegido pelo manipulador thread-específico. Dentro deste procedimento ocorre uma exceção e o manipulador thread-específico é chamado. Dentro do manipulador, pergunta-se ao usuário se o manipulador deve ou não engolir a exceção. Se o usuário decidir que o manipulador deve engolir a exceção, o programa está preparado para continuar rodando, mas termina normalmente. Se o usuário decidir que a exceção não deve ser engolida pelo manipulador, então o manipulador final é acionado (durante o fechamento do programa). Na vida real, este manipulador seria responsável pelos logs e registros, pelo fechamento de manipuladores de arquivo, liberação de memória, etc. Mas, antes que o programa finalmente termine, algo muito interessante acontece. O sistema chama o manipulador de exceções thread-específico caso mais desobstruções sejam necessárias nesta moldura de pilha particular que usa dados locais. Este é o desdobramento feito pelo sistema. Todos estes eventos são anunciados por várias caixas de mensagem que aparecem na tela.
O programa exept2