Livro sobre C e Linux

Hoje decidi abrir uma exceção e postar esse link aqui. É um livro que escrevi há 9 anos e nunca foi revisado nem publicado, mas que ainda pode ser útil para muitos novos programadores.

 


unix shell

O interpretador de comandos dos sistemas operacionais Unix e Unix-like é a interface tradicional para operar o computador. Por meio de comandos, o usuário direciona o sistema para tarefas específicas.

O interpretador, conhecido como shell, normalmente suporta uma linguagem de script para que os comandos possam ser executados de maneira sequencial, permitindo automatizar tarefas rotineiras.

Mas, internamente, o que faz o shell? Bem, as etapas básicas são as seguintes:

  1. imprime o prompt de comando: $ para usuário ou # para root
  2. lê a linha digitada pelo usuário
  3. remove CRLF (\r\n) e depois separa a linha em strings, onde houver espaço
  4. fork(): duplica o conteúdo do próprio processo (parent) no sistema, criando um processo filho (child)
    • no child: execvp() executa o comando digtado – tipo ls -l
    • no parent: wait(NULL) aguarda o processo filho terminar

Embora os shells mais avançados como bash e csh possuam uma enorme quantidade de funções, tais como as da biblioteca readline, um shell apenas com as etapas mencionadas acima funciona perfeitamente.

Aqui está o exemplo:

/* 20090118 - AF */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

static char *stripcrlf(char *line)
{
    char *p;
    for(p = line; *p; p++)
        if(*p == '\r' || *p == '\n') { *p = '\0'; break; }
    return line;
}

static void split(char *line, char **args)
{
    while(*line) {
        while(*line && *line == ' ') *line++ = '\0';
        *args++ = line;
        while(*line && *line != ' ') line++;
    }
}

int main()
{
    char *p, line[4096], *args[sizeof line/2];

    for(;;) {
        fprintf(stdout, getuid() ? "$ " : "# ");
        fflush(stdout);

        memset(line, 0, sizeof line);
        if(!fgets(line, sizeof line, stdin)) {
            fprintf(stdout, "\n");
            clearerr(stdin);
            continue;
        }

        p = stripcrlf(line);
        if(*p) split(p, args);
        else continue;

        if(!fork()) {
            if(execvp(args[0], args) == -1) perror("exec");
        } else wait(NULL);
    }
    return 0;
}

NOTA: a imagem no topo foi criada por Paul Bourke em 1989, encontrada neste site.


linux fmemopen, bsd funopen

Enquanto desenvolvia um sistema, me deparei com um problema entre plataformas, relacionado ao uso e manipulação de arquivos temporários em memória.

Ao invés de gravar um arquivo temporário no disco e ir populando com o conteúdo, meu caso exigia que esse arquivo fosse populado em memória para uso posterior, e a API do Linux (via libc) fornece a função fmemopen(3).

Porém, meu ambiente de desenvolvimento é no Mac OS X, e devido ao tipo do software, era inevitátel tanto desenvolver como testar tudo nessa mesma máquina, para então posteriormente colocá-lo em produção em um Linux. O problema é que o OS X herdou a API do BSD, que ao invés de fmemopen, fornece apenas o funopen(3).

Por esse motivo, acabei escrevendo uma versão bem simples do fmemopen do Linux para usar no OS X, usando funopen internamente. O único detalhe, e provavelmente a diferença entre a versão original do fmemopen pra minha, é que a minha simplesmente ignora o modo de acesso ao arquivo (“r”, “w”, etc). O fato é que agora posso continuar desenvolvendo tudo no OS X e depois compilar no Linux obtendo o mesmo resultado.

fmem.h:

/*
 * fmem.h : fmemopen() on top of BSD's funopen()
 * 20081017 AF
 */

#ifndef _FMEM_H
#define _FMEM_H

#ifndef linux
#include <stdio.h>
extern FILE *fmemopen(void *buf, size_t size, const char *mode);
#else
#define _GNU_SOURCE
#endif

#endif /* fmem.h */

fmem.c:

/*
 * fmem.c : fmemopen() on top of BSD's funopen()
 * 20081017 AF
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifndef linux
struct fmem {
    size_t pos;
    size_t size;
    char *buffer;
};
typedef struct fmem fmem_t;

static int readfn(void *handler, char *buf, int size)
{
    int count = 0;
    fmem_t *mem = handler;
    size_t available = mem->size - mem->pos;

    if(size > available) size = available;
    for(count=0; count < size; mem->pos++, count++)
        buf[count] = mem->buffer[mem->pos];

    return count;
}

static int writefn(void *handler, const char *buf, int size)
{
    int count = 0;
    fmem_t *mem = handler;
    size_t available = mem->size - mem->pos;

    if(size > available) size = available;
    for(count=0; count < size; mem->pos++, count++)
        mem->buffer[mem->pos] = buf[count];

    return count; // ? count : size;
}

static fpos_t seekfn(void *handler, fpos_t offset, int whence)
{
    size_t pos;
    fmem_t *mem = handler;

    switch(whence) {
        case SEEK_SET: pos = offset; break;
        case SEEK_CUR: pos = mem->pos + offset; break;
        case SEEK_END: pos = mem->size + offset; break;
        default: return -1;
    }

    if(pos < 0 || pos > mem->size) return -1;

    mem->pos = pos;
    return (fpos_t) pos;
}

static int closefn(void *handler)
{
    free(handler);
    return 0;
}

/* simple, but portable version of fmemopen for OS X / BSD */
FILE *fmemopen(void *buf, size_t size, const char *mode)
{
    fmem_t *mem = (fmem_t *) malloc(sizeof(fmem_t));

    memset(mem, 0, sizeof(fmem_t));
    mem->size = size, mem->buffer = buf;
    return funopen(mem, readfn, writefn, seekfn, closefn);
}
#endif

E finalmente, um programa simples para testar o uso da função, que compila e produz o exato mesmo resultado tanto no Linux como no OS X – não testei em outros BSDs, mas suponho que deva funcionar também.

test.c:

/*
 * test.c : memory fp test
 * 20081212 AF
 */

#include "fmem.h"
#include <stdio.h>
#include <string.h>

int main()
{
    FILE *fp;
    char buff[128], temp[128];

    memset(buff, 0, sizeof buff);

    fp = fmemopen(buff, sizeof buff, "r+");
    fprintf(fp, "hello world");
    fseek(fp, 0, SEEK_SET);

    memset(temp, 0, sizeof buff);
    fgets(temp, sizeof temp, fp);

    fclose(fp);

    fprintf(stdout, "read: [%s]\n", temp);

    return 0;
}

Para compilar, basta o usar o seguinte Makefile:

CC=gcc
RM=rm -f
NAME=test

.SUFFIXES = .o
OBJECTS = test.o fmem.o

.c.o:
    $(CC) -Wall -c -o $@ $< all: $(OBJECTS)     $(CC) -Wall -o $(NAME) $(OBJECTS) clean:     $(RM) $(NAME) $(OBJECTS)[/sourcecode]


interceptando ping

Depois de umas discussões sobre como interceptar pacotes ICMP que chegam no kernel, vim escrever esse artigo e registrar o código dos programas que fazem isso, da maneira mais simples possível.

Na verdade, o procedimento pra interceptar o ICMP echo request (ping), é o mesmo de qualquer sniffer de pacotes, e por isso precisa ser executado como root. Isso ocorre porque o programa que intercepta precisa criar um socket do tipo RAW, e essa função do kernel não pode ser usada por usuários comuns.

Embora o código seja relativamente simples, envolve bastante teoria sobre programação com sockets, e conhecimento (nesse caso) do protocolo ICMP e suas características. Aquele papo furado que você aprendeu na faculdade, com os cabeçalhos de pacotes na forma de desenho colorido não serve pra nada aqui – pois aqui, os cabeçalhos de pacotes são estruturas de linguagem C.

Quando um programa solicita a criação de um SOCK_RAW pro kernel, é possível especificar um determinado protocolo, pra que o próprio kernel já faça um filtro e não envie coisas que você não quer. Assim, posso especificar o protocolo identificado pelo número 1 (veja no /etc/protocols, é o ICMP). Há ainda uma função, tanto em C quanto em python, chamada getprotobyname, onde você especifica o nome do protocolo e recebe o número como resposta – mas isso é bem juvenil, embora seja um padrão.

Depois de criar o SOCK_RAW, já filtrando o conteúdo por ICMP, basta sair capturando os pacotes com recvfrom. Apesar dessa função retornar, entre outros, o endereço IP do remetente do pacote, o cabeçalho IP inteiro estará disponível nos dados do pacote. Ao ler de um RAW socket filtrado por ICMP, você terá o seguinte nos dados do pacote:

  1. cabeçalho IP
  2. cabeçalho ICMP
  3. dados

Basicamente, o kernel picota o ethernet frame / ARP e manda o resto pro programa. Abaixo, o programa que faz essa malandragem e sai mostrando na tela os pacotes ICMP recebidos na sua máquina:

/* icmpd.c 20080704 AF
 *
 * print incoming icmp echo-request
 * compile:
 *  cc -Wall icmpd.c -o icmpd
 * references:
 *  /usr/include/netinet/ip_icmp.h
 *  SOCK_RAW socket(2)
 */

#include <stdio.h>
#include <fcntl.h>
#include <netdb.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>

// raw packet
struct packet {
    struct ip ip;
    struct icmp icmp;
    char data[4096];
};

int main()
{
    struct packet pkt;
    int fd, len, srclen;
    struct sockaddr_in src;

    if((fd = socket(AF_INET, SOCK_RAW, 1)) == -1)
        perror("socket"), exit(1);

    for(;;) {
        srclen = sizeof(src);
        memset(&pkt, 0, sizeof(pkt));
        memset(&src, 0, sizeof(src));
        len = recvfrom(fd, &pkt, sizeof(pkt), 0, (struct sockaddr *)&src, (socklen_t *)&srclen);

        if(len >= 1 && !pkt.icmp.icmp_type)
            fprintf(stdout, "icmp echo-request sequence %d with %d bytes from %s\n",
                    ntohs(pkt.icmp.icmp_hun.ih_idseq.icd_seq), len-20, inet_ntoa(src.sin_addr));
    }

    return 0;
}

Tem vários truques escondidos ai nesse programa. Primeiro, o que é lido pelo recvfrom “encaixa” no tipo de dados chamado packet, que foi criado lá em cima contendo a struct ip, struct icmp e um campo de dados que só armazena até 4096 bytes, menos o tamanho dos outros dois cabeçalhos. Se o programa receber um pacote ICMP com mais dados que isso (tipo, ping -s 5000 localhost), ele irá imprimir o tamanho do pacote errado.

Segundo, que depois do recvfrom, só entra no if quando o ICMP é do tipo 0 (echo reply), e não tipo 8 (echo request). Aí, quando o ICMP é do tipo zero, aquela union chamada icmp_hun (vide /usr/include/netinet/ip_icmp.h), provê os dados da struct ih_idseq, ao invés de qualquer outro dentro dela – quem não sabe como funciona union já viaja nessa parte; mas não desanime, procure no google que é fácil.

Pra fechar, imprimir o número de sequência do pacote requer uma conversão, visto que esse número está armazenado em network byte order, e não host byte order! Por isso, é necessário usar aquele ntohs() malandro ali. Não por menos, o tamanho do pacote len-20 é pra subtrair os 20 bytes do cabeçalho ip, e mostrar o tamanho correto, igual ao ping.

Um outro detalhe é que esse programa está baseado na struct icmp do BSD, porque estou usando um mac osx aqui. Porém, ele é totalmente compatível com lunix sem necessidade de alterar o código – os kernel/libc developers do lunix são muito malandros e deixam os headers (ip.h, ip_icmp.h, etc) no esquema.

Se você se empolgou com isso, eu não podia deixar por menos e me sinto quase que obrigado a colocar uma versão desse programa em python. É tão simples… veja aí:

#!/usr/bin/env python
# icmpd.py 20080704 AF
#
# print incoming icmp echo-request
# references:
#  /usr/include/netinet/ip_icmp.h
#  http://docs.python.org/lib/module-struct.html

import struct, socket

def parse(pkt):
    raw_size = len(pkt)
    pkt_size = raw_size - 1

    # set format and unpack
    format = '!B' + (pkt_size and '%ds' % pkt_size or '')
    rawdata = struct.unpack(format, pkt)

    # check for echo-reply
    if rawdata[0] or not pkt_size: return None

    # extract reply sequence number
    sequence = struct.unpack('!hh', rawdata[1][3:7])
    return (raw_size, sequence[1])

if __name__ == '__main__':
    fd = socket.socket(socket.AF_INET, socket.SOCK_RAW, 1)

    while 1:
        chunk, src = fd.recvfrom(4096)
        obj = parse(chunk[20:])
        if obj:
            print 'icmp echo-request sequence %d with %d bytes from %s' % (obj[1], obj[0], src[0])

O princípio é o mesmo. A diferença é que no python não é possível acessar aquelas structs do kernel direto, e por isso é necessário usar o módulo struct (hehe). Esse também tem vários truques, veja…

Logo depois de ler os dados, já envio pra função parse(), o chunk[20:], que passa os dados à partir dos 20 primeiros bytes – ou seja, já picota o cabeçalho IP que estava ali. Dentro da função parse, crio um formato tipo ‘!B60s’, que significa:

  1. o exclamação determina que os dados estão em network byte order
  2. o primeiro B especifica um unsigned char, o primeiro campo da struct icmp – que contém o tipo!
  3. o número acompanhado de “s”, especifica um campo char com N bytes, que não será modificado

Esse unpack retorna uma lista com apenas dois índices – o primeiro com o tipo do pacote ICMP, e o segundo com o resto do cabeçalho.

Se eu quisesse, por exemplo, pegar os três primeiros campos da struct icmp, usaria algo assim: ‘!BBh’. Veja no /usr/include/netinet/ip_icmp.h que os campos são: unsigned char type, unsigned char code e unsigned short checksum. Se comparar com o manual do módulo struct do python, verá que ‘!BBh’ significa a mesma coisa, e o exclamação ali pra indicar o formato dos dados – network byte order.

Por fim, pra pegar o número de sequência do pacote tem outro truque. Como o rawdata[0] era o tipo do pacote icmp, e o rawdata[1] era o resto do cabeçalho ICMP, era necessário pegar apenas aquela struct ih_idseq lá dentro, que possui dois campos do tipo unsigned short. Considerando que o rawdata[1] era o cabeçalho, à partir do campo “code”, pulei três bytes – 1 do code, e 2 do checksum. À partir dali, mandei ler os outros 32, sendo 16 de cada unsigned short – icd_id e então icd_seq.

Por isso, aquele struct.unpack(‘!hh’, rawdata[1][3:7]) retorna exatamente os dois campos da struct icmp e permite obter o número de sequência do pacote.


teste de turing

Com certeza você já ouviu falar na tal Máquina de Turing. Depois de umas aulas lá no Prandiano, vários anos atrás, escrevi um programa que faz aquela brincadeira de criança: pense em um número de 0 a 10 – e aí consigo advinhar o número.

Respostas de SIM e NÃO determinam o resultado. Tudo que você precisa é ser capaz de responder de maneira adequada.

Veja aí, pensei no número 13:

$ ./turingame
pense em um número de 1 a 31 e pressione enter...

01 03 05 07 09 11 13 15 17 19 21 23 25 27 29 31
esta número está aqui? [s/n]: s

02 03 06 07 10 11 14 15 18 19 22 23 26 27 30 31
esta número está aqui? [s/n]: n

04 05 06 07 12 13 14 15 20 21 22 23 28 29 30 31
esta número está aqui? [s/n]: s

08 09 10 11 12 13 14 15 24 25 26 27 28 29 30 31
esta número está aqui? [s/n]: s

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
esta número está aqui? [s/n]: n

o número é 13!

Legal, né. Talvez o código não seja tão legal, nem tão intuitivo. Sabe como é… mas, pra mais informações, você pode ver os detalhes da Máquina de Turing e Teste de Turing (inglês) no wikipédia, nos links abaixo:

Máquina de Turing
Turing Test

Na sequência, o código do programa. É de 2004, tem até goto. Nem adianta torcer o nariz, porque duvido que um programador nunca tenha usado coisas como goto, system() e popen() – este último usa /bin/sh -c pra executar, exatamente como o você-sabe-quem.

/*
 * para compilar:
 * cc -Wall turingame.c -o turingame -lm
 *
 * turingame.c 20040726 AF
 */

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

int getlist(int i, int max_num)
{
   int j, n = pow(2, i);

   for(j = 1; j < max_num/n; j+=2) {
      int r = j * n;

      for(i = 0; i < n; i++, r++)
        fprintf(stdout, "%02d ", r);
   }

   fprintf(stdout, "\n");
   return n;
}

int main(int argc, char **argv)
{
   int i, r = 0, max_pow = 5, max_num = 0;

   if(argc > 1) {
      if(*argv[1] == '-' && argv[1][1] != 'h') {
         char s[2] = { argv[1][1], 0 };
         max_pow = atoi(s);
         if(!max_pow) goto help;
      } else {
help:
         fprintf(stderr, "uso: %s [-h|-potência(padrao=%d)\n", *argv, max_pow);
         return 0;
      }

   }

   max_num = pow(2, max_pow);
   fprintf(stdout, "pense em um número de 1 a %d e pressione enter...\n",
         max_num-1); fflush(stdout);
   getchar();

   for(i = 0; i < max_pow; i++) {
      int n;
      char c;
      n = getlist(i, max_num);
      fprintf(stdout, "esta número está aqui? [s/n]: "); fflush(stdout);
      c = getc(stdin); getc(stdin);
      if(c == 's') r+=n;
      fprintf(stdout, "\n");
   }

   fprintf(stdout, "o número é %d!\n", r);

   return 0;
}