Jump to content
  • Especificações da linguagem C

       (0 reviews)

    Há algum tempo tinha vontade de escrever algo sobre os padrões da linguagem C e recentemente, motivado por uma discussão na lista exploits-brasil [1] e aproveitando um domingo nublado no Rio de Janeiro, decidi começar.

    Este artigo vai ajudá-lo a entender (e quando utilizar) os padrões de programação da linguagem C. Não ache que a utilidade é teórica, pois não é. Seguir um padrão pode ajudar na segurança de código, otimização e até aumentar a produtividade, como veremos mais a diante (sabia que C tem o tipo boolean, por exemplo?).

    1. Introdução

    A maioria das linguagens de programação seguem padrões abertos, que os criadores de compiladores (ou interpretadores) precisam seguir para suportar a linguagem completamente. A linguagem C foi criada por Denis Ritchie (falecido em 2011) entre 1969 e 1973 com o objetivo de substituir o Assembly (o que explica a existência comandos como goto na linguagem). Desde então, várias versões de compiladores foram criadas, por várias pessoas e empresas, sem seguir uma especificação oficial. Por isso, em 1983, o American National Standards Institute, mais conhecido pela siga ANSI, formou um comitê para estabelecer um padrão, que só foi concluído em 1989.

    2. Linha do tempo da linguagem C

    • 1969-1973: Denis Ritchie cria a linguagem C
    • 1983: O ANSI cria um comitê para especificar a linguagem
    • 1989: A especificação ANSI X3.159-1989 é concluída. O nome informal dela é C89 e também é conhecida por ANSI C.
    • 1990: O ISO (International Organization for Standardization) assume a especificação de linguagem e cria um grupo de trabalho (working group) chamado WG14 [2] para cuidar da especificação do C, que é renomeada para ISO/IEC 9899:1990, informalmente conhecida como C90. Não houve mudanças na linguagem. Esta especificação se mantém por vários anos e define a linguagem C que “todos conhecem”.
    • 1999: O WG14 publica a especificação ISO/IEC 9899:1999, informalmente chamada de C99. Esta já traz várias novidades que serão tratadas mais à frente neste artigo.
    • 2011: O WG14 publica a ISO/IEC 9899:2011, conhecida como C11. Este é o padrão atual da linguagem.

    Vários recursos cobertos nas especificações mais recentes já eram implementados pelos desenvolvedores de compiladores por sua conta. No caso do GCC, por exemplo, o Projeto GNU criou os padrões GNU89, GNU99 e GNU11 com suas próprias extensões. O Visual C++ da Microsoft também tem várias extensões da linguagem (lembre-se que em geral qualquer compilador C++ compila C, pois a linguagem é a mesma, mas a recíproca não é verdadeira).

    Vou mostrar alguns recursos das especificações atuais, utilizando o gcc 4.6 para os exemplos.

    A opção -std do gcc define qual especificação usar. Já a -pedantic faz o gcc alertar sobre qualquer construção que seja proibida pela especificação, de forma bem rigorosa.

    3. Recursos da C99

    • Tipo booleano:

    Incluindo a stdbool.h, você pode declarar variáveis do tipo bool e usar os estados verdadeiro e falso, assim:

    #include <stdbool.h>
    
    void main()
    {
    	bool var;
      
    	if (var)
    		var = false;
    }

    É fato que isso já era feito usando-se 1 e 0 e na verdade os estados true e false não passam de outros nomes (#define) para estes números, respectivamente. No entanto, a legibilidade do código melhora com o tipo bool.

    • Comentários com //

    A C89 só permite comentários entre /* e */, o que dá um trabalho extra para comentários de uma linha. Usando a C99, você pode iniciar a linha com //, como no C++, PHP etc. e esta linha será um comentário perfeitamente válido.

    • Funções em linha

    Quando você chama uma função no código, o compilador normalmente gera uma instrução CALL (do Assembly) para desviar o fluxo de código para o endereço da função. Isso envolve passagem de parâmetros pela pilha de memória (stack), retorno etc. Ou seja, isto gera um ônus de processamento e consumo de memória considerável, principalmente associado ao número e tamanho dos argumentos. Um código que não use funções é mais otimizado, no entanto, é mais difícil de manter. Para resolver isso, você pode dizer que uma função é em linha (inline). Dessa forma, toda vez que a função for chamada, o código dela será copiado para o trecho onde a chamada aconteceu. É como se você copiasse e colasse a função em vários trechos, mas sem precisar fazer isso.;)

    Imagine que temos a função:

    inline int soma(int num1, num2)
    {
    	return num1 + num2;
    }

    Como ela foi declarada como inline, ao chamá-la por exemplo como:

    var = soma(3, 5);

    O compilador vai substiuir por:

    var = 3 + 5;
    • Vetores de tamanho variável

    Na C99 você pode declarar um vetor assim:

    void funcao(int numero)
    {
    	char vet[numero];
    }

    O tamanho do array de char vet depende do parâmetro recebido. Isso era resolvido antes com funções para alocação dinâmica.

    Há adições também do tipo complex, para números complexos (e imaginários) e muito mais novidades. No site do WG14 [2] há toda a especificação para download.

    Para utilizar a C99 com o gcc, use:

    $ gcc -std=c99 fonte.c

    4. Recursos da C11

    • Novas funções seguras (com bound checking)

    As funções tidas como inseguras como strcpy, strcat etc ganharam novas versões sufixadas com “_s”, de safe. Basicamente elas recebem um argumento a mais que é o número de caracteres para trabalhar. São elas:

    • Multithreading (thread.h)

    Apesar de os SOs modernos fornecerem funções de API para multithreading, ao ser inserida oficialmente no C possivelmente vai ajudar na portabilidade de aplicativos que utilizem este recurso. É algo muito bom para a linguagem.

    • Estruturas e uniões anônimas

    Útil quando estão aninhados, este foi um recurso que senti falta quando programei a primeira versao do pev, que usa muitas estruturas. Se queremos que um campo de uma estrutura seja outra estrutura, antes da C11, é preciso fazer:

    struct ponto {
    	int x;
    	int y;
    }
    
    struct reta {
    	struct ponto p;
    }

    Aí para acessar:

    struct reta r;
    
    r.p.x = 10;

    Com a C11, a estrutura reta pode ser:

    struct reta {
    	struct ponto;
    }

    E o acesso:

    r.x = 10;

    Dizemos então que a estrutura reta possui uma estrutura anônima (sem nome) como campo.

    5. Qual especificação usar?

    Na hora de usar uma especificação e seguir suas regras, você deve pesar como é o seu projeto, em quais sistemas ele vai rodar, como será distribuído etc. Por exemplo, se é um projeto interno e o compilador que você utiliza suporta a C99, não vejo por que não utilizá-la. Agora, se você vai distribuir seu código, aí já tem que pensar nas versões dos compiladores de quem for baixar. Será que o gcc do usuário do seu software é a última versão? Será que suporta a C11?

    No hdump, um dumper hexa/ASCII que fiz para usar no Windows e no Solaris, eu optei por trabalhar com C89 por dois motivos: o código é pequeno e eu precisava levar o fonte para compilar em versões antigas do Solaris, com compiladores que não suportavam a C99, por exemplo.

    Já para o pev um analisador de binários de Windows, eu comecei em ANSI mas já estou atualizando para C99, para deixar o código mais legível e utilizar os novos recursos. Eu distribuo o fonte e binários já compilados, inclusive para Windows, então não tem problema para os usuários.

    O importante é ler as especificações e perceber se tem algo que te ajuda/atrapalha no projeto, pensando no escopo, usuários e sistemas alvo. Só a C11 que acho que ainda não dá para distribuir um código em produção, visto que nenhum compilador à suporta completamente. Recomendo usá-la internamente e esperar um pouco antes de liberar código com ela, pois está muito recente.

    6. Conclusão

    Existem outros recursos interessantes na C11 [3] mas ainda não conheço um compilador que a suporte 100%. Na verdade o gcc também não suporta a C99 por inteiro ainda. Existe até uma página onde você pode acompanhar as mudanças no suporte à C99 [4]. Na versão 4.6, o gcc já estreiou o suporte inicial à C11, sob a opção -std=c1x. A versão 4.7 já vai vir com suporte a mais recursos desta especificação, sob a opção -std=c11. Inslusive cabe um comentário: é muito bom poder contar com um conjunto de compiladores constantemente atualizado e bem feitos como são os do GCC. Essa maravilhosa suíte inclui compiladores para C, C++, Objective-C, Fortran, Java, Ada, e Go. Todos livres. É um espetáculo mesmo. :)

    O último rascunho ISO/IEC 9899:201x, divulgado em abril do ano passado está disponível em PDF [5] mas a especificação mesmo, publicada em dezembro, está à venda por cerca de R$ 450,00 na ISO Store [6].

     

    Referências

    [1] https://groups.google.com/forum/?fromgroups#!forum/exploits-brasil
    [2] http://www.open-std.org/JTC1/SC22/WG14/
    [3] http://en.wikipedia.org/wiki/C11_%28C_standard_revision%29
    [4] http://gcc.gnu.org/gcc-4.6/c99status.html
    [5] http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
    [6] http://www.iso.org/iso/iso_catalogue/catalogue_tc/catalogue_detail.htm?csnumber=57853


    User Feedback

    Join the conversation

    You can post now and register later. If you have an account, sign in now to post with your account.

    Guest

  • Similar Content

    • By julio neves
      Livro do Julio Cezar Neves com dicas importantes (e raras de serem encontradas) sobre shell, incluindo sincronismo de processos, novidades do Bash 4.0, uso do ImageMagik e YAD (o melhor da categoria dos dialog da vida). Vale ler cada palavra. ?
       
      E se quiser ver se tem turma aberta do curso dele é só clicar aqui. ?
    • By Fernando Mercês
      Dia 02/04/2019 (terça) tivemos o lançamento oficial do Visual Studio 2019, com o anúncio de inúmeras novidades envolvendo o desenvolvimento de soluções baseadas em tecnologias como Azure DevOps, .NET Core, ASP.NET Core, C# e PowerShell.

      Assim como aconteceu em outras ocasiões, a Microsoft novamente fará uma parceria com comunidades técnicas através da realização de eventos locais.

      O DevOps Professionals em conjunto com a FC Nuvem também participa desta iniciativa, com um EVENTO PRESENCIAL e GRATUITO 

      Programação prevista (grade sujeita a alterações):

      - Novos Recursos para Debugging no Visual Studio 2019 + Suporte a Docker no .NET Core 3.0 - Renato Groffe (Microsoft MVP)

      - Dicas e truques com Azure e Azure DevOps no Visual Studio 2019 - Vinicius Moura (Microsoft MVP)

      - Colaboração Contínua com o Visual Studio Live Share - Milton Câmara Gomes (Microsoft MVP)

      - Indo além de ambientes Windows com PowerShell Core, Linux e Visual Studio Code - Ewerton Jordão (.NET SP, SampaDevs)

      Acompanhe e apoie esta iniciativa, divulgando e indicando o Visual Studio 2019 Launch para amigos e colegas de trabalho!
      Mais informações: https://www.sympla.com.br/visual-studio-2019---lancamento---devops-professionals--fc-nuvem__525409
    • By anderson_leite
      Olá, já faz um bom tempo desde do ultimo artigo sobre a construção de debuggers mas, sem mais delongas, vamos dar continuidade a esta série! ? 
      Neste artigo iremos falar um pouco sobre uma chamada de sistema que é capaz de controlar quase todos os aspectos de um processo: a syscall PTRACE (process trace). Antes de continuarmos, vale ressaltar que todo o código utilizado neste artigo está disponível no repositório do Github.
      De acordo com o manual do Linux (man ptrace), a syscall ptrace é definida assim:
      "A syscall ptrace provê meios para que um processo (denominado "tracer") possa observar, controlar a execução de um outro processo (denominado "tracee"), examinar e modificar a memória e registradores do "tracee". É primariamente utilizado para a implementação de 'breakpoint debugging' e para rastreamento de syscalls".
      Em outras palavras, podemos utilizar a ptrace para controlar um outro processo sobre o qual termos permissões sobre!
      Por exemplo, execute:
      strace /bin/ls O comando "strace" acima, é utilizado para que se possa rastrear todas as syscalls que um programa realiza. Vale lembrar que toda a técnica utilizada para o rastreamento de syscalls envolve o conteúdo abordado nos artigos anteriores, então é de suma importância que você tenha lido (ou saiba) o primeiro artigo sobre Sinais e o segundo sobre Forks.
      Antes de começar a rastrear um dado comando, o strace precisa ter controle total sobre a execução do processo alvo, para isso é feito um fork do processo em questão e o mesmo é "traceado". Voltaremos neste assunto em breve.
      A wrapper da ptrace é definida em <sys/ptrace.h> e tem o seguinte protótipo:
      #include <sys/ptrace.h> long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); Onde o primeiro argumento request é um enum onde cada valor define uma ação em cima do "tracee", tais como TRACEME, GETEREGS, SETREGS e etc. O segundo argumento, pid, é o PID (Process Identification) do processo que queremos "tracear", o terceiro argumento addr é um endereço para alguma interação que a ser realizada da memória do processo "traceado" e o quarto e último argumento data é algum tipo de dado passado para o processo.
      Agora que você ja conhece o formato desta syscall, vamos fazer um pequeno breakdown do comando "strace".
      Execute:
      strace strace /bin/ls 2>&1 | grep -A2 clone Por mais bizarro que o comando acima pareça, o que vamos fazer aqui é rastrear todas as syscalls que o strace faz usando o próprio strace! Como a saída padrão do strace não é o stdout (dê uma lida em standart streams, caso esteja confuso) então é primeiro redirecionar a saída de erro para a saída padrão, para que seja possível rodar o grep no que queremos.
      Estamos buscando aqui, alguma chamada a syscall clone, que é sempre chamada quando é feito um fork. A chamada à ptrace vem logo em seguida:
      clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c4aa8ea10) = 16203 ptrace(PTRACE_SEIZE, 16203, NULL, 0) = 0 Nesse caso, o strace cria um processo filho e em seguida usa o ptrace com o argumento SEIZE para iniciar o rastreamento (tracing) de um processo sem interrompê-lo, como analisaremos em seguida. Dessa maneira o strace é capaz de interceptar cada chamada de sistema feita pelo processo!
      Dê uma olhada no comando ltrace, que diferente do strace, rastreia todas as chamadas à bibliotecas (libraries trace) e tente fazer o mesmo que fizemos acima!
      Algumas ações notáveis que podemos fazer com a ptrace:
      PTRACE_PEEKTEXT, PTRACE_PEEKDATA Ler uma word em um dado endereço. PTRACE_POKETEXT, PTRACE_POKEDATA Copiar uma word para um determinado endereço (injete dados na memória). PTRACE_GETREGS Ler os registradores de um processo, que será guardado na struct user_regs_struct em <sys/user.h>. PTRACE_SETREGS Escrever nos registradores de um processo (também no formato da struct acima). Execute "man ptrace" para uma abordagem mais detalhadas de todos os valores disponíveis. ?
       
      Implementando um simples tracer
      Agora que já temos uma base de forks e uma ideia de como o ptrace funciona, podemos unificar os dois e tenho certeza que o ptrace irá ficar mais claro. A partir de agora ele é fundamental para a implementação do nosso debugger.
      O primeiro passo é definir o escopo de como será feito o nosso "tracer": vamos rastrear um processo que já esta sendo executado ou vamos criar um novo? Para o nosso debugger, iremos apenas criar um fork e trocar sua imagem de execução para a do programa que queremos debugar, usando uma das funções da família exec.
      Primeiro vamos usar a função execl, que faz parte do leque de funções exec (man 3 exec) que trocam a imagem do nosso processo por outra, ou seja, o nosso programa é realmente trocado por outro em uma execução.
      A função execl é definida como:
      #include <unistd.h> int execl(const char *pathname, const char *arg, ... /* (char *) NULL */); Onde o primeiro argumento pathname é caminho completo do nosso executável alvo e os demais argumentos, que podem ser vários, são os argumentos para o programa que será executado.
      Para seguir um padrão, o primeiro argumento que geralmente colocamos é o caminho do programa em questão (lembrem que no array argv a posição 0 guarda o nome do programa em si), o resto dos argumentos são opcionais e seguem no modelo de lista de argumentos que são delimitados por um argumento NULL, que geralmente usamos para finalizar a lista.
      Agora considere o seguinte exemplo:
      #include <unistd.h> #include <stdio.h> int main(int argc, char* const* argv) { if (argc < 3) { printf("Usage: %s <command> <args>\n", argv[0]); return 1; } const char* command = argv[1]; char* const* args = &argv[1]; printf("First arg => %s\n", args[0]); execv(command, args); puts("Continua?\n"); return 0; } Compile com
      $ gcc -o exec exec.c $ ./exec /bin/ls -lah Este programa bem simples demonstra como a exec funciona.
      O que acabamos de criar aqui foi uma espécie de wrapper para qualquer comando: ele irá pegar o nome do comando e os seus respectivos argumentos e trocar sua execução atual pela a que você especificou.
      Note também a string "Continue?" que deveria ser impressa na tela. Esta nunca será impressa pois o nosso programa virou de fato, outro.
      Interessante, não? Usando um pouco de criatividade, podemos criar novos processos filhos combinando forks + exec, ou seja, criamos um fork do nosso processo e trocamos sua imagem por outra! Dessa maneira, por exemplo, temos total controle sobre o comando ls.
      Modificando um pouco o código acima e seguindo a ideia de forks, temos:
      #include <stdio.h> #include <sys/types.h> #include <sys/ptrace.h> #include <unistd.h> int main(int argc, char* const* argv) { if (argc < 3) { printf("Usage: %s <command> <args>\n", argv[0]); return 1; } const char* command = argv[1]; char* const* args = &argv[1]; pid_t child_pid = fork(); // Neste ponto, todas as variaveis sao copiadas para o nosso fork // o fork NAO recebe as mesmas variaveis, apenas uma cópia ;) if (!child_pid) { // Hora de transformar nosso fork em outro programa ptrace(PTRACE_TRACEME, NULL, NULL, NULL); execv(command, args); } char in; do { puts("Iniciar processo ? [y/n]: "); in = getchar(); } while (in != 'y'); ptrace(PTRACE_CONT, child_pid, NULL, NULL); return 0; } Compile
      $ gcc -o fork_exec fork_exec. $ ./fork_exec /bin/ls O programa acima realiza os primeiros passos do nosso tracer: é passado o caminho de um programa e os argumentos para o mesmo. Com isso criamos um fork e usamos o ptrace no própio fork com o argumento TRACEME. Este parâmetro indica que o este processo será "traced" pelo seu processo pai. Em seguida trocamos a nossa execução para o nosso programa alvo. Neste momento temos total controle sobre a execução, no exemplo acima, do comando ls.
      Quando um processo inicia sua execução com TRACEME + exec, o mesmo recebe um sinal de interrupção (SIGTRAP) até que o seu processo pai indique que ele deve continuar sua execução. Por isso, o nosso processo pai, que retém o PID do processo filho, usa o ptrace com o argumento CONT para que seja enviado o signal para dar continuidade de execução.
      E depois?
      Agora toda a comunicação entre os processos pai e o filho se dará via sinais e usaremos a syscall wait constantemente.
      Lembra que definimos acima algumas funções que podemos usar em conjunto com a ptrace? Para já irmos adiantando alguns artigos, vamos fazer um programa que mostra o estado dos registradores para um processo, passo a passo. Vamos usar dois parâmetros para a ptrace: GETREGS e STEP. Segue o código:
      #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/ptrace.h> #include <sys/user.h> #include <sys/wait.h> void display_regs(struct user_regs_struct* regs) {     printf("RIP: 0x%x\n", regs->rip);     printf("RBP: 0x%x\n", regs->rbp);     printf("RSP: 0x%x\n", regs->rsp); } int main(int argc, char* const* argv) {     if (argc < 2) {         fprintf(stderr, "Usage: %s <program_path>\n", argv[0]);         return 1;     }     const char* progName = argv[1];          pid_t child = fork();     if (!child) {         ptrace(PTRACE_TRACEME, NULL, NULL, NULL);         execl(progName, progName, NULL);     }          int status;     int options = 0;     int signal;     // Estrutura que mantem os registradores     struct user_regs_struct regs;     /// Capta primeiro sinal de parada do filho     waitpid(child, &status, 0);     signal = WSTOPSIG(status);     if (signal == SIGTRAP) {         printf("Processo alvo %s esperando pronto para iniciar\n\n", progName);     }          printf("Executando 10 instruções\n");     for (int i = 0; i < 10; ++i) {         printf("Passo: %d\n", i+1);         // Executa uma instrução         ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);         // Espera sinal do filho         waitpid(child, &status, 0);         // Copia o estado atual dos registradores         ptrace(PTRACE_GETREGS, child, NULL, &regs);         // Função local para imprimir os principais registradores         display_regs(&regs);         puts("\n\n");     }     puts("Continuando...\n");     /// Continua execução     ptrace(PTRACE_CONT, child, NULL, NULL);     waitpid(child, &status, 0);     printf("Filho saiu com %d\n", WIFEXITED(status));     return 0; }  
      Compile:
      $ gcc -o tracer tracer.c $ ./tracer /bin/ls O código acima, além de criar e rastrear o processo, executa as primeiras 10 instruções e copia os estados dos registradores em cada passo. Logo após, continua a execução do programa normalmente.
      A estrutura user_reg_struct, definida em <sys/user.h>, contém todos os registradores que estão disponíveis na sua arquitetura. O código foi escrito considerando um ambiente x86-64.
      Com o estudo da ptrace, fechamos toda a introdução para construirmos o nosso debugger de fato, que vamos começar a desenvolver no próximo artigo, incialmente com capacidade de por breakpoints, imprimir o atual estado dos registrados e executar instrução por instrução do processo.
      Qualquer dúvida ou correção sinta-se livre de por nos comentários!  ?
      Links úteis:
      Process control Process relationship Code injection with ptrace Sinais Fork Até a próxima!
    • By void_
      https://bookauthority.org/books/new-networking-books
      E aí, concordam com a lista acima? Confesso que muitos títulos me chamaram a atenção, mas antes de fazer algum movimento imprudente ($$), gostaria de ouvir alguma opinião de alguém que possa ter tido a oportunidade de ter comprado, lido, analisado, etc., um ou mais dos títulos da lista. Se alguém puder fornecer algum pdf, mesmo que seja prévia, também serei grato.
      P.S: Os livros de C e Python particularmente me interessaram...
×
×
  • Create New...