thegoodfellas/blogset-br huggingface dataset:
'a') e fazemos isso com o movl 28(%esp), %edx ai temos que acessar esse ponteiro movl (%edx), %edx e armazenar em edx o valor contido no endereco que estava armazenado em 'p' (sendo q 'p' contem o endereco de 'a'). Agora que ja vimos bastante o LEA, vamos traduzir algumas utilizacoes do LEA para portugues:leal (%ebx), %eaxEsta instrucao copia o endereco da expressao (%ebx) para eax. Como o "endereco efetivo" e o valor de ebx, essa instrucao vai copiar ebx em eax como se fosse um MOV. Nada muito util :)leal 3(%ebx), %ebxCopia o valor de ebx + 3 para ebx. Nao muito util ja que um ADD faria o mesmo, possivelmente mais rapido.leal 3(%ebx), %eaxAqui temos uma utilizacao mais sensata de LEA, ela vai copiar o valor de ebx somado a 3 em eax. Isso significa que fizemos um MOV + ADD em uma unica instrucao! Ok, e o que LEA tem com MUL!?!? Se usarmos a capacidade de LEA operar calculos com enderecos sem acessar a memoria podemos usar isso para multiplicar por 2,4,8 ou ate por 3,5,9, ou ate executar essa multiplicacao e somar um inteiro. Usando LEA para multiplicar:leal (%eax,%eax,2), %eax // EAX = EAX * 2 + EAX = EAX*3leal (%eax,%eax,4), %eax // EAX = EAX * 4 + EAX = EAX*5leal (%eax,%eax,8), %eax // EAX = EAX * 8 + EAX = EAX*9Outros exemplo:leal 8(,%eax,4), %eax // Multiplica eax por 4 e adiciona 8No Asm AT&T a sintaxe displacement(base register, offset register, scalar multiplier) e equivalente ao [base register + displacement + offset register * scalar multiplier] na sintaxe da Intel. Um ou ambos os parametros numericos e um dos registradores podem ser omitidos. Sendo que o fator de escala deve ser 1,2,4 ou 8. Ok! Viram! Com LEA vc pode realizar alguns calculos (e sem modificar os flags! isso consta na documentacao da Intel). Se voce consultar qualquer guia de otimizacao de codigo (principalmente para guiar compiladores) vera que LEA pode fornecer metodos bem uteis de realizar operacoes matematicas! E isso pessoal! Meu parecer e esse tal gato que meu brother fez nao e tanto um gato, e muito mais uma tecnica muito boa!!!!
Ponteiros sao os grandes bichos-papoes de quem esta querendo aprender C, e ponteiro para funcao e o uber monstro para a maioria dos programadores novatos. Mas, basta um pouco de paciencia e um olhar destemido para perceber que nao existe nada de tao assustador sobre eles. Assumindo que voce conhece ponteiros vou me concentrar no topico ponteiro para funcoes. E importante saber distinguir uma declaracao de funcao de um ponteiro para funcao. Nos exemplos abaixo:- int * func();- int (*func)();O primeiro caso define uma funcao que retorna um ponteiro para um inteiro, ja o segundo caso e um ponteiro para uma funcao que retorna int. Lembre-se ( ) tem precedencia ao * logo o primeiro caso sera avaliado como uma funcao e o segundo como um ponteiro (*func) para uma funcao int (). Ponteiros para funcao nao sao somente um jeito complicado de programar, sao extremamente uteis, pois podem substituir longas cadeias de if/else ou um switch gigante, sao uteis para criar callbacks, e, de certa forma, sem eles nao existiria como implementar mecanismos de ligacao dinamica de funcoes como DLL's ja que voce solicita um nome na tabela de simbolos do dll, e a partir de um ponteiro de funcao voce consegue chamar a funcao cujo codigo foi carregado na memoria possivelmente apos a imagem do seu processo ter sido carregado. Um ponteiro para funcao funciona pois, no final das contas, o codigo das funcoes tambem estao na memoria e se o controle da cpu for transferido para esse endereco o codigo sera executado, claro que o endereco tem que ser um endereco aonde seu processo tem privilegios de execucao. Para declarar um ponteiro para uma funcao utilizo mentalmente um jeito bem simples, e que funciona bem na maioria dos casos. Esse metodo consiste em escrever como se voce estivesse declarando a funcao, por exemplo:int f(int a);Entao coloque parenteses ao redor do nome da funcao e um asterisco na frente do nome:int (*f)(int a);Note que nao e necessario colocar o nome da variavel que e recebida como parametro, mas talvez seja uma boa para fim de entendimento do codigo. Ok, agora vamos continuar a brincadeira e pensar grande, quem sabe um ponteiro para uma funcao que receba um inteiro retorne um ponteiro para um float? Simples, primeiro escreva como se estivesse declarando:float * f(int i);Depois, faca a jogada que aprendemos anteriormente:float * (*f)(int i);Pronto! Viu como a coisa nem e tao complicada assim! Antes de demonstrar aspectos praticos vamos declarar mais uns ponteiros para funcoes que pareceriam intragaveis antes. Digamos, um ponteiro para uma funcao que retorna um array de ponteiros para funcoes que retornam um ponteiro void e nao recebem parametros. Somando a isso, a funcao recebe como parametro dois inteiros e um ponteiro para int, hehehe bem ate a declaracao disso e meio bizarro, mas vamos la:void * (**f(int a, int b, int * pi))(void)Ok, isso merece uma explicacao hehehe, f e o nome da funcao, ela recebe dois inteiros e um ponteiro para inteiro, o retorno dela e um ponteiro para ponteiro **f e esse ponteiro e para
oper2);break; case '/': result = Divide (oper1, oper2);break; } printf("Resultado: %f %c %f = %f\n",oper1,opCode,oper2,result); }int main() { Solve(2,3,'+'); Solve(7,3,'-'); Solve(2,7,'*'); Solve(25,5,'/'); getchar(); return 0; }Utilizando uma funcao que utiliza ponteiros de funcao como parametro, ou seja, recebe uma funcao como parametro, ficaria parecido com:#include <stdio.h>float Plus (float a, float b) { return a+b; }float Minus (float a, float b) { return a-b; }float Multiply (float a, float b) { return a*b; }float Divide (float a, float b) { return a/b; }float Solve(float oper1, float oper2, float (*f)(float,float)) { return ((*f)(oper1,oper2)); }int main() { printf("Resultado: 2 + 3 = %f\n",Solve(2,3,Plus)); printf("Resultado: 7 - 3 = %f\n",Solve(7,3,Minus)); printf("Resultado: 2 * 7 = %f\n",Solve(2,7,Multiply)); printf("Resultado: 25/5 = %f\n",Solve(25,5,Divide)); getchar(); return 0; }E obvio que este caso e um caso trivial e nao existe necessidade real de utilizar um ponteiro para funcao. A coisa muda de figura em casos aonde algum trabalho extra e pre-requisito para o funcionamento das operacoes, imagine receber os operandos via RPC ou que seja necessario converter a entrada do usuario em uma GUI para valores float a fim de obter os operandos, ou qualquer coisa do tipo. O codigo para executar esta tarefa nao teria que ser replicado em todos os casos do switch bastaria codificar uma vez e baseado na necessidade chamar passando o ponteiro para a funcao correta. Essa tecnica de passar uma funcao para outra e chamada de callback, e a funcao sendo chamada geralmente recebe o nome de callback function. Normalmente callbacks sao utilizadas quando e necessario que duas camadas diferentes de um sistema se comuniquem como, por exemplo, voce passa uma funcao para o SO de forma que, quando um evento qualquer ocorrer o sistema chame essa funcao pra voce. Note que fiz um apontamento * quando falei sobre passar uma funcao para outra, no caso de C/C++ nao e isso que ocorre de fato. Em linguagens imperativas como e o caso de C/C++, normalmente nao permitem tal operacao o que estamos fazendo e meramente passar o endereco de onde a funcao esta na memoria. Funcoes que recebem outra funcao como parametro, e de fato implementam funcoes de alta ordem gerais so sao possiveis em linguagens funcionais e nao vou tratar disso neste tutorial e possivelmente nao neste blog heheheh. Entao e isso, da proxima vez que pensar em ponteiros para funcao ao inves de medo, sorria, pois e um recurso muito poderoso da linguagem e que nao e tao complicado quanto dizem por ai!That's all folks!
Seja por economia de espaco ou por pura preguicite aguda e comum que uma boa parte dos programadores escolha usar '\n' para quebrar um linha, principalmente para logar informacoes de debug no console. No entanto, qual e a diferenca? Bem a diferenca principal e que o std::endl faz um flush no buffer (para streams que suportam buffer) entao se voce nao quer que o buffer receba um flush com muita frequencia e melhor usar o \n, mas se voce quer evitar o flush automatico do buffer e deseja ter um controle melhor de quando o buffer sera comitado use std::endl.De um ponto de vista simplista:std::cout << std::endl;// e equivalente astd::cout << "\n" << std::flush;Claro que quem estiver usando o operador '<<' deve saber que operacoes de I/O, mesmo bufferizadas, sao operacoes lentas. Entao nao leve o assunto performace TAO a serio nesse cenario. Em contra partida lembre que o flush pode criar algumas chamadas a mais e se o seu codigo e uma classe de log em arquivo no disco, por exemplo, entao voce pode ter uma perda de performace SIM. Vale lembrar que isso acontece pois, no geral, escrever um bloco de bytes e mais economico do que escrever varios bytes um de cada vez, os dispositivos sao desenhados para efetuar operacoes com multiplos bytes entao aproveite essa feature! E lembre-se que no geral, para arquivos abertos em modo texto, o newline sera mapeado para o caracter correto de fim de linha(LF em Unix, CRLF no DOS(Windows), CR no Macintosh, e por ai vai), vale lembrar que cout e cin sao arquivos especiais, ou uma especie de arquivo(se voce prefere assim) e tambem vao se comportar como tal. Para nao manter o artigo tao superficial vamos olhar a implementacao do endl na libstdc++ da GNU:/*** @brief Write a newline and flush the stream.** This manipulator is often mistakenly used when a simple newline is* desired, leading to poor buffering performance. See* http://gcc.gnu.org/onlinedocs/libstdc++/27_io/howto.html#2 for more* on this subject.*/templatebasic_ostream<_CharT, _Traits>& endl(basic_ostream<_CharT, _Traits>& __os) { return flush(__os.put(__os.widen('\n'))); }Veja que a documentacao e bem clara quanto ao comportamento do endl, mas olhando o codigo fica claro o que acontece. O endl recebe o stream que vai operar como parametro e chama o put para inserir um '\n'. O retorno do put sera enviado para o flush causando, obviamente, um flush na stream.That's all!!! Alguns videos do jogo enquanto estou fazendo so para acompanhar andamento e evolucao. Trata-se de um clone, o mais fiel possivel(meu possivel inclui minha burrice e tambem preguica =D) do Megaman 1 do NES. Feito em C++ e Allegro, testo no windows 7 e no linux (ubuntu), mas deve rodar em qualquer lugar que suporte a lib Allegro. Sempre gostei de historias onde pessoas contam seus casos de trincheira, casos onde solucoes complexas e, algumas vezes, totalmente fora da caixa foram essenciais para que um problema fosse solucionado. Nao somente casos envolvendo programadores, todos os campos do saber tem dessas historias e sao sempre interessantes! Se me recordo bem, mais ou menos a 9 meses atras foi o ultimo caso desse tipo
que tive, antes de eu ser catapultado de forma abrupta da vida de programador. Infelizmente foi um lance bem kafkaesco mas, lamurias a parte, vamos la!Existem varios niveis de pesadelo quando voce esta trabalhando com um sistema legado. Talvez o sistema nao tenha nenhuma documentacao, o que e pessimo, mas se o codigo for bem escrito voce estara bem. Mesmo se for mal escrito, o codigo esta ai! use o metodo de dividir em partes pequenas e ir entendendo, e reescrevendo o que for necessario. Pode ser que voce nao esteja lidando diretamente com codigo legado, tem vez que o pesadelo vem na forma de um device fabricado na Galileia e com um manual em aramaico, justo quando o especialista em aramaico da equipe esta ocupado com outras areas do sistema.Mas, mesmo o texto estando em um idioma que voce nao domina, usualmente o codigo estara escrito numa linguagem de programacao que voce conhece. Entao voce copia e cola o que tem no manual, contando com uma certa ajuda divina e vai ajustando ate que o device faca o que tem que fazer de forma aceitavel, e um processo terrivel e que toma muito tempo, sem contar que o codigo resultante funciona sem ninguem saber ao certo o porque, mas funciona. Outro caso acontece quando o um dos desenvolvedores e obrigado a trabalhar no natal e nomeou as variaveis de porco_com_polenta, papai_noel, renas (CASO REAL) e o sistema e uma mistura profana de COM+ e loucura (Redundante quando se trata de COM+), mas com ferramentas atuais de edicao de codigo e alguma paciencia, so o COM+ sera um problema.So que algumas vezes o universo conspira contra voce, no meu caso o problema veio na forma de uma biblioteca que gerava erros, as vezes eles eram esporadicos e causavam a quebra do sistema, outras vezes eles eram aparentemente inofensivos, mas sempre que o codigo executava em um debugger, as ferramentas piscavam como um arvore de natal doidona de LSD. E o maior problema dessa biblioteca e que o codigo fonte foi perdido! Perdido? Sim, perdido! Ate onde fui informado, o codigo estava em um disquete que estragou e tudo o que restou foi uma versao ja compilada no repositorio.Para melhorar, a empresa onde eu estava trabalhando tinha regras sobre instalacao e utilizacao de software com licenca de avaliacao, sem contar com diversos NDAs dizendo que eles vao sequestrar seu cachorro se qualquer linha de codigo do sistema vazar para qualquer midia que seja. Entao, mesmo que voce tenha pensado anteriormente: Facil, baixo o IdaPRO, pego o que ele gera, ja organizado, e posto uma duvida no stackoverflow. Nao, caro desconhecido, voce nao tera essa opcao. Esse sistema vinha sendo portado entre diversas plataformas (OS/2, Cygwin, Diversas distribuicoes e versoes diferentes de kernel do linux) e, nessa danca macabra, o fonte se perdeu. O modulo era muito importante, mas por questoes de tempo nunca houve apoio gerencial para parar a equipe e reescrever tudo. O erro que ele causava nao era uma preocupacao imediata, entao os anos foram passando e ficou por isso mesmo.Um
deve a forma como ele gerencia a memoria do programa sendo instrumentado. Em vermelho notamos que o valgrind indica que houve um read invalido de 4 bytes. Ele informa tambem que esse acesso foi em uma regiao de memoria depois de um bloco de 40 bytes alocado pela malloc (na minha maquina um int tem tamanho 4 bytes por isso o bloco alocado tem tamanho 40). Ele nos diz, tambem, que essa chamada a malloc foi realizada na funcao main. Com essa informacao e seguro assumir que a funcao tentou acessar um indice N+1 no buffer passado pela main, o buffer apontado por int * data.Ainda em vermelho, o valgrind nos diz o offset dentro do processo onde ocorreu esse read invalido. O codigo ofensivo esta dentro da imagem do processo pois, no nosso caso ficticio, a lib foi compilada estatica no programa (i.e: todo codigo e copiado direto para o executavel resultante).Temos que ver o codigo da funcao compute_some_stuff de alguma forma, no caso a ferramenta objdump era a minha unica alternativa. O objdump, como diz o nome, exibe diversas informacoes sobre um ou mais arquivos objeto. Uma dessas funcoes e exibir o codigo assembly do codigo contido no arquivo. Nota para os que nao sabem nada de assembly: Voce deve aprender! E indispensavel saber ler o asm da plataforma onde voce desenvolve, e sua obrigacao =D Isso vale para a turma das linguagens baseadas em VM, se voce nunca olhou o bytecode gerado voce esta falhando como programador!Saber assembly permite entender melhor como seu programa realmente funciona na maquina, alem de permitir resolver problemas que seriam impossiveis caso voce nao saiba. Primeiro vamos ver quais funcoes existem na lib e quais estao sendo exportadas.$ objdump -t libbroken.a In archive libbroken.a:broken_lib.o: file format elf64-x86-64SYMBOL TABLE:0000000000000000 l df *ABS* 0000000000000000 broken_lib.c0000000000000000 l d .text 0000000000000000 .text0000000000000000 l d .data 0000000000000000 .data0000000000000000 l d .bss 0000000000000000 .bss0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack0000000000000000 l d .eh_frame 0000000000000000 .eh_frame0000000000000000 l d .comment 0000000000000000 .comment0000000000000000 g F .text 0000000000000041 compute_some_stuffOtimo, a unica funcao (F) dentro da lib e a compute_some_stuff, e ela e global (g) isso vai tornar a saida em assembly mais facil de ler.$ objdump -d libbroken.aIn archive libbroken.a:broken_lib.o: file format elf64-x86-64Disassembly of section .text: 0000000000000000 : 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 89 7d e8 mov %rdi,-0x18(%rbp) 8: 89 75 e4 mov %esi,-0x1c(%rbp) b: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp) 12: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 19: eb 19 jmp 34 1b: 8b 45 f8 mov -0x8(%rbp),%eax 1e: 48 98 cltq 20: 48 c1 e0 02 shl $0x2,%rax 24: 48 03 45 e8 add -0x18(%rbp),%rax 28: 8b 00 mov (%rax),%eax 2a: 83 e0 fe and $0xfffffffe,%eax 2d: 01 45 fc add %eax,-0x4(%rbp) 30: 83 45 f8 01 addl $0x1,-0x8(%rbp) 34: 8b 45 f8 mov -0x8(%rbp),%eax 37: 3b 45 e4 cmp -0x1c(%rbp),%eax 3a: 7e df jle 1b 3c: 8b 45 fc mov -0x4(%rbp),%eax 3f: c9 leaveq 40: c3 retq Antes de tentar entender como ela funciona e bom saber exatamente em
qual instrucao ela cometeu o read invalido, para saber essa informacao devemos analisar qual instrucao se encontra no offset 0x4006A6 do programa principal, que e onde o valgrind diz ter ocorrido o read:$ objdump main_prog -d[CODE]...4006a6: 8b 00 mov (%rax),%eax...[MORE CODE]O que ele esta fazendo e desreferenciando o que esta em rax que certamente e um ponteiro para nosso buffer, e colocando o valor em eax. Essa mistura de um registrador de 64bits com a porcao 32bits dele mesmo se deve ao fato de que ponteiros, na minha maquina, tem 8bytes so que ele aponta para um buffer de integers de 4 bytes, por isso a mistura de rax e eax. Durante um desses acessos o programa fez referencia para memoria invalida. Vamos focar em entender a funcao nos atendo a como esse read aconteceu e nao aos detalhes do funcionamento interno da mesma.Vamos organizar o codigo por partes para ignorar o que nao for relevante: Primeiro o setup do frame da funcao, nada que importe para nosso caso:push %rbpmov %rsp,%rbpO codigo que segue pode parecer curioso para quem esta acostumado com asm 32bits, pois ele esta salvando em variaveis locais o valor dos registradores rdi e rsi:mov %rdi,-0x18(%rbp)mov %esi,-0x1c(%rbp)Mas a razao e simples, eis um pedaco do codigo da funcao main quando vai chamar compute_some_stuff:mov -0x10(%rbp),%raxmov $0xa,%esimov %rax,%rdicallq 40067eEla passa os parametros nos registradores ao inves de colocar na pilha. Isso se deve a uma diferenca importante entre o i386 system V ABI e o amd64 system V ABI. Estou usando uma maquina 64 com um kernel 64. Na i386 System V ABI os argumentos para uma funcao sao passados na pilha e somente pela pilha. Ja no amd64 System V ABI os argumentos sao passados primeiro nos registradores (rdi, rsi, rdx, rcx, r8 e r9 se o argumento for inteiro e nos registradores xmm0..xmm7 se for um float). Somente quando esses registradores foram todos utilizados e que a pilha e usada.A seguir a funcao zera duas variaveis locais:movl $0x0,-0x8(%rbp)movl $0x0,-0x4(%rbp)Dai, entao, a funcao salta imediatamente para o offset 0x34:jmp 34 No offset 0x34 encontramos:34: mov -0x8(%rbp),%eax37: cmp -0x1c(%rbp),%eax3a: jle 1b Ate o momento, sabemos que a funcao zera duas variaveis locais e salta para o offset relativo 0x34, la ela move para eax uma dessas variaveis locais (variavel_1) e compara o valor do registrador com o parametro que foi passado para a funcao (vou assumir, baseado no codigo da funcao main ao fazer a chamada, que seja o parametro data_len, ate porque nao faria muito sentido comparar com o ponteiro do buffer). Se (variavel_1) for menor ou igual ao (data_len) a funcao retorna o controle para o offset 0x1b:1b: mov -0x8(%rbp),%eaxEntao o codigo recupera novamente a variavel local(variavel_1) e armazena em eax. Neste ponto ja podemos ter uma visao da organizacao das variaveis locais e dos parametros da funcao:E facil notar que estamos olhando para um loop. Passando o olho no codigo vamos notar que essa variavel e sempre incrementada em 1, antes de um salto condicional (jle):30: addl $0x1,-0x8(%rbp)Vamos organizar as ideias. O codigo salta direto para um teste,
caso esse teste seja avaliado como verdadeiro ela salta de volta e executa algum codigo, sempre incrementando variavel_1 em uma unidade, e valido assumir que estamos olhando para um laco for:int variavel_1;for (variavel_1 = 0; variavel_1 <= data_len; variavel_1++)BINGO! Nao precisamos entender mais nada do funcionamento interno da funcao. Basta lembrar que nos passamos como parametro o tamanho do nosso buffer, se o loop e controlado por uma condicao <=, ele sempre vai acessar um item a mais do que deveria. Por sinal, sabemos que ele usa <= pelo uso da instrucao jle, que significa jump if less or equal. Qualquer coisa, alem do google, claro, olhe os manuais da intel, sao otimos.E agora? Agora vamos realizar um patch binario nessa lib e mudar a instrucao de jle para jl(jump less) que tera o efeito de mudar o teste para: variavel_1 < data_len. Isso vai corrigir o problema do loop, claro que podem existir outros erros na funcao, mas vamos corrigir o mais obvio primeiro. Uma olhada no manual da intel de instrucoes (ou qualquer site ou livro que lhe apetecer) nos mostra que a instrucao jle, para saltos curtos, usa o opcode 0x7E, ja a instrucao jl usa o opcode 0x7C. Entao basta substituir o byte correto na lib, compilar novamente o programa e testar.Para realizar esse trabalho existem diversas maneiras, uma delas e usar um editor hexadecimal, na epoca optei por escrever um programinha em C que abre a lib e atualiza o byte. Primeiro ache em qual offset esse byte esta dentro da lib, uma ideia e usar a ferramenta hexdump em conjunto com o grep para filtrar a saida.$ hexdump libbroken.a | grep 7e00000e0 8948 e87d 7589 c7e4 f845 0000 0000 45c70000110 458b 3bf8 e445 df7e 458b c9fc 00c3 4347Vale lembrar que a ferramenta hexdump, na configuracao default, agrupa os bytes em words de 16bits e como estou usando uma maquina little endian a ordem dos bytes estarao trocados na visualizacao, entao o par destacado em negrito estara, na verdade, ordenado como 7e df no arquivo. Essa sequencia lembra algo?3a: 7e df jle 1b Exato! Achamos o nosso jump. E o opcode da instrucao esta no offset 0x110 + 6. Fiz o seguinte programa:patch.c:int main(){ FILE * fp = fopen("libbroken.a", "rb+"); fseek(fp, 0x116, SEEK_SET); fputc(0x7C, fp); fclose(fp); return 0;}Vamos testar?$ gcc -o patch patch.c$ ./patch$ gcc -o main_prog main_prog.c -L. -lbroken$ ./main_prog [40]Ho Ho Ho! Agora a funcao retorna o valor que deveria. Vamos ver o codigo dela em assembly:$ objdump -d libbroken.aIn archive libbroken.a:[CODE]...37: 3b 45 e4 cmp -0x1c(%rbp),%eax3a: 7c df jl 1b 3c: 8b 45 fc mov -0x4(%rbp),%eax...[MODE CODE]Nosso patch funcionou perfeitamente e agora a funcao retorna o valor correto. Mas, sera que passa no valgrind? Vejamos:$ valgrind ./main_prog==4503== Memcheck, a memory error detector==4503== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.==4503== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info==4503== Command: ./main_prog==4503== [40]==4503== ==4503== HEAP SUMMARY:==4503== in use at exit: 0 bytes in 0 blocks==4503== total heap usage: 1 allocs, 1 frees, 40 bytes allocated==4503== ==4503== All heap blocks were freed
-- no leaks are possible==4503== ==4503== For counts of detected and suppressed errors, rerun with: -v==4503== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)Agora nossa lib se comporta corretamente, retornando o valor esperado e sem gerar nenhuma operacao de READ invalida. Muito bem, basta levantar da cadeira do trabalho e ir pra casa programar!!! That's all folks!