Linux na AT91SAM9263-EK - Parte 1

Uma das coisas legais dessa placa é a abundância de informações e coisas prontas. A Atmel fornece os fontes do bootstrap, o U-Boot e o kernel do Linux também a suportam. Podemos baixar e instalar os programas e ferramentas, podemos baixar um ambiente pronto ou podemos construir a própria infraestrutura, e é isso que vou descrever neste texto. Esta é uma coleção de scripts, não um repositório de fontes de programas e bibliotecas, por isso, os scripts foram construídos para fazer o download dos fontes, preferencialmente a partir dos sites oficiais. Entretanto, alguns programas tem pequenos bugs ou precisam de algum tipo de adaptação e, nesse caso, arquivos contendo as correções/alterações (patches) são fornecidos. Às vezes são necessárias ferramentas de apoio que não existem e precisaram ser construídas por mim, os fontes são fornecidos com licença GPL 3.0.

Mas, antes de continuar, um aviso: Os sistemas/scripts construídos tem fins didáticos, embora a utilização para quaisquer outros fins, dentro da lei, seja livre, não há nenhuma garantia de funcionamento, como devidamente explicado na licença GPL3. A licença dos scripts é GPL3, a menos que esteja especificado o contrário no cabeçalho, a licença dos textos é FDL 1.3. As licenças dos programas-fonte é a que está publicada em cada um deles. Se usar qualquer parte deste texto como referência para cursos, palestras, projetos, trabalhos, outros artigos ou qualquer outra utilização legal, seja educado e mencione a fonte. Se possível, gostaria de ser avisado. Os scripts funcionaram no Ubuntu 10.10.

A primeira coisa a fazer é construir as ferramentas necessárias para poder compilar os outros programas. Depois, vamos compilar os programas construídos para o Tabajara Linux para que funcionem nessa CPU. Quanto mais "para baixo", maior é a diferença entre as plataformas. O bootstrap e o U-boot são específicos para a placa. O toolchain e a libc também tem que ser modificados para funcionar com ARM. Já os programas de nível mais alto dificilmente precisarão ser modificadas, basta compilar e vai estar pronto para usar. Resumindo, uma vez que as ferramentas de compilação e os programas para inicialização do hardware estiverem funcionando, em tese qualquer programa poderá ser compilado para funcionar no Tabajara ARM Linux. Em tese, é claro.

Começando pelo começo, do ponto de vista da inicialização, quando a placa é ligada ou resetada, um programa que está gravando na ROM interna entra, e tenta detectar, carregar para a SRAM e executar um programa contido na dataflash, se não der certo, tenta o mesmo na NANDFlash, e, por último, tenta encontrar um arquivo chamado boot.bin num SD card. Se, ainda assim, nada for encontrado, a EEPROM é vasculhada. Se não houver um programa na EEPROM, o SAM-BA é iniciado e fica esperando uma transação pela porta USB device ou na porta DBGU.

O protocolo do SAM-BA é ridiculamente simples, mas por hora é melhor usar a ferramenta pronta fornecida pela Atmel. Ela carrega um programinha na SRAM, dependendo do tipo de operação, e executa-o. Existe um programa para gravação na NAND, outro para a Dataflash, etc. Essa aplicação também permite executar um script TCL para automatizar as operações, mas infelizmente sou analfabeto em TCL. Já falei sobre o SAM-BA anteriormente, vale a pena dar uma olhada lá.

O U-boot é um programa sensacional capaz de funcionar em um quantidade enorme de processadores e placas, é um programa monitor com um shell simples, e também é um boot loader capaz de carregar e inicializar diversos tipos de kernel, entre eles o Linux. O kernel pode ser carregado a partir de Bootp, TFTP, NFS, Storage Device ligado na porta USB, porta serial usando protocolo Xmodem e, obviamente, NAND flash, NOR, eeprom, etc. Durante a fase de desenvolvimento é mais fácil e produtivo usar o U-boot para carregar o kernel e o sistema de arquivos a partir da rede, evitando a regravação da flash e ficar toda hora precisando usar a ferramenta SAM-BA. Quando o desenvolvimento estiver mais estável, o kernel e o sistema de arquivos podem (devem?) ser gravados na flash.

O kernel do Linux será configurado para suportar todos os periféricos da placa, e ainda alguns dispositivos USB, como câmeras, wi-fi dongle, etc. Além disso, é mais fácil adicionar tudo e depois retirar o que não vai ser usado. Além das ferramentas básicas, vários programas serão compilados, como o UDev, o links e o wpa_supplicant.

Melhorias nos scripts

Com o aumento na quantidade de ferramentas, bibliotecas e utilitários, foi necessário criar um sistema para verificar as dependências entre eles. Sem isso, pode ser que o build não termine ou, ainda pior, que compile incorretamente. Também foi adicionado um modo de instalar dependências de binários. A filosofia é que cada script é responsável de instalar tudo que for necessário para funcionar corretamente. Como o i586, montamos um espelho do sistema de arquivos que vai ser usado, com todos os diretórios e arquivos. Entretanto, não usamos a habilidade de instalar que vem com os programas, porque eles instalam muitas coisas além do básico (por exemplo, exemplos, testes, documentação, etc), e queremos somente os binários e nada além do que for estritamente necessário. Por isso, usamos o instalador embutido para instalar tudo no diretório da arquitetura (onde estão os compiladores) e copiamos somente o que nos interessa para o diretório raiz do novo sistema de arquivos. Quando o sistema de arquivos estiver com todas os programas instalados, ele pode ser copiado, do jeito que está, diretamente para um dispositivo, por exemplo, um cartão de memória, ou, ainda, poderemos gerar uma imagem de um sistema de arquivos que pode ser copiada para um dispositivo. Também poderemos exportar o diretório e montar via rede.

Toolchain

O mesmo conjunto de scripts que usado para construir o TBJ/i586 será usado para o TBJ/at91, com algumas pequenas modificações. A glibc, por exemplo, não é oficialmente suportada na arquitetura ARM, o suporte é fornecido através de um plugin. O binutils tem um bug no as, que aparece quando compilando no gcc 4.4, e que precisa ser consertado, o script aplica um patch para consertar. Já o gcc compila sem problemas.

UDev

O uso do Udev é questionável para sistemas embutidos, mas ele pode ser útil em certas situações, por exemplo, se for permitido adicionar/remover dispositivos via USB. De qualquer forma, remover o UDev é simples: basta não iniciar o daemon. No kernel selecionamos a opção de montar o /dev automaticamente, o que evita a necessidade de termos um script para criar os devices na inicialização. No diretório /etc/scripts existem alguns shell scripts que foram construídos para serem executados quando um determinado tipo de hardware for detectado. Alguém poderá alegar que isso é justamente o que o UDev faz, mas não com shell scripts.

Scripts de inicialização

Uma pequena alteração nos scripts: para ficar mais parecido com um SystemV tradicional, movi os scripts para "/etc/init.d" e usei links para inicializar (começando com S) e finalizar (começando com K). Os scripts detectam se a intenção é iniciar ou finalizar, e também aceitam os tradicionais start/stop, se passados como parâmetro da linha de comando. Os scripts ainda precisam ser melhorados, mas a infraestrutura está razoavelmente estabelecida.

Plataforma "generic"?

Na primeira tentativa, copiei todos os scripts desenvolvidos para o i586 e fui alterando para que funcionassem no AT91. Entretanto, logo ficou claro que a maioria dos scripts podem ser usados sem qualquer modificação em mais de uma plataforma, exceto, é claro, as informações da arquitetura alvo e o compilador. Por isso, movi todos os scripts comuns para o diretório "generic", junto com o template comum de root e o script de funções comuns.

Árvore de Diretórios

Após essas adaptações e melhorias, a árvore de diretórios ficou assim:

tbj
scripts
generic scripts que (teoricamente) servem para qualquer plataforma.
common.sh script com funções de uso geral.
app scripts para compilar os programas.
base scripts para compilar o kernel e o busybox.
lib scripts para compilar as bibliotecas.
root arquivos com templates para montagem de sistema de arquivos, genéricos.
toolchain scripts para geração do toolchain (compilador, linker, libc, etc).
armat91 scripts para plataforma ARM AT91
all.sh script para compilação de todos os scripts.
vars.sh script que define as variáveis da plataforma.
app scripts para compilar programas.
base scripts para compilar o kernel e o busybox.
buildcontrol diretório onde é mantido o controle de compilação e versões.
lib scripts para compilar as bibliotecas.
preboot scripts para construir ferramentas de pré-boot.
root arquivos com templates para montagem de sistema de arquivos, específicos para a plataforma.
toolchain scripts para geração do toolchain (compilador, linker, libc, etc).
utils utilitários diversos (sam-ba, scripts).
i586pc scripts para plataforma i586
(Repete a estrutura do diretório armat91)
arm-at91-linux-gnu Criado durante o build, tudo o que for produzido pelos scripts do diretório armat91 ficará nesse diretório.
bin diretório dos executáveis do toolchain, também pode servir para instalação de algum utilitário que precise ser colocado no diretório /bin. Por enquanto somente programas pertencentes ao toolchain são colocados aqui.
build diretório onde os programas e bibliotecas são compilados. Alguns programas não conseguem ser compilados em um diretório diferente do diretório onde estão os fontes, quando isso acontece, para manter o padrão, um soft link é criado neste diretório, apontando para o diretório real dos fontes.
arm-at91-linux-gnu algumas bibliotecas do GCC são colocadas neste diretório
lib diretório de instalação das bibliotecas, corresponde ao /lib.
root Este não é o diretório do usuário root, mas o root da imagem do novo sistema de arquivos. Má escolha de nome.
src contém os fontes dos diversos programas e bibliotecas.

usr diretório de instalação dos programas e bibliotecas, corresponde ao /usr.
i586-tbjpc-linux-gnu Criado durante o build, tudo o que for produzido pelos scripts do diretório i586pc ficará nesse diretório.
(Repete a estrutura arm-at91-linux-gnu)
tar arquivos-fonte compactados.

Boot

É necessário um pequeno programa de inicialização que prepara o hardware e carrega um outro programa. Esse programa tem de ser mínimo e agnóstico, e tem de saber conversar com o hardware para poder prepará-lo. Ainda bem que a Atmel fornece os fontes de um programa de inicialização, já que não somos especialistas no hardware e construir o programa de inicialização do zero poderia ser um pouco trabalhoso. Com os fontes do programa em mãos, poderemos fazer algumas modificações. Sem alterações, o programa fornecido pela Atmel faz o seguinte:

    Inicializa o hardware e o controlador de memória
    Lê 1 bloco de dados da memória flash (NAND ou NOR, dependendo do fonte escolhido) para a RAM
    Executa o programa carregado (um simples jump para o endereço de carga)

U-Boot

O programa de inicialização pode carregar um outro programa intermediário ou o kernel do Linux diretamente. Esse "programa intermediário" pode ser, por exemplo, o U-Boot. Ele tem suporte já pronto para diversas placas, incluindo a nossa, digo, do Fernando, e é capaz de usar a USB, o LCD, as portas seriais, as Flashes, e, muito importante, a ethernet. Além de "entender" o hardware, o U-Boot traz suporte pronto para fazer o download de programas pela serial usando XModem, pela ethernet usando tftp ou nfs, e a inicialização pode ser feita com IP fixo ou dinâmico usando DHCP ou bootp. Podemos definir o funcionamento do U-Boot facilmente usando "variáveis de ambiente" que são carregadas quando o U-Boot é iniciado. Podemos modificar o conteúdo das variáveis e executar alguns comandos para inspecionar o hardware usando um shell simples, acessado via porta serial. Com toda essa flexibilidade, o U-Boot é altamente recomendado durante a fase de desenvolvimento, e muitas empresas distribuem seus produtos com o U-Boot em produção. A desvantagem é perder um pouco do espaço em flash.
O script que compila o U-Boot e gera os arquivos no formato correto para serem gravados na flash é o bootloader/2-uboot.sh. Defina as variáveis de ambiente SERVERIP e DEVICEIP no arquivo bootloader/3-uboot_gen_files.sh para os endereços IPs fixo do servidor NFS e do dispositivo. A variável CMDLINE_NFS deve conter a linha de comando a ser passada pelo kernel. Se usado como está, o endereço IP do servidor está definido como 192.168.20.1 e o do device é 192.168.20.100.

Formato do kernel

O kernel precisa estar no formato esperado pelo U-Boot. Felizmente, junto com os próprios fontes do Ubuntu vem uma ferramenta capaz de criar um arquivo no formato correto, é o mkimage, que é corretamente chamado pelo programa bootloader/3-uboot_gen_files.sh.

U-Boot carregando o kernel e o sistema de arquivos na flash

Para essa opção, execute o script bootloader/scripts/uboot-nand-nand.tcl pelo SAM-BA. Essa opção é ideal para usar o U-Boot em produção. O layout da nand flash será o seguinte:

Endereço Conteúdo
0x00000000 Bootloader
0x00020000 U-Boot
0x00080000 U-Boot Environment
0x000A0000 Kernel
0x00400000 Sistema de Arquivos (root)

U-Boot com o kernel na flash e o sistema de arquivos remoto

Para essa opção, execute o script bootloader/scripts/uboot-nand-nfs.tcl pelo SAM-BA. O layout da nand flash será o seguinte:

Endereço Conteúdo
0x00000000 Bootloader
0x00020000 U-Boot
0x00080000 U-Boot Environment
0x000A0000 Kernel

U-Boot com o kernel e o sistema de arquivos remotos

Para essa opção, execute o script bootloader/scripts/uboot-nfs-nfs.tcl pelo SAM-BA. Essa opção é particularmente interessante para testar modificações no kernel. O layout da nand flash será o seguinte:

Endereço Conteúdo
0x00000000 Bootloader
0x00020000 U-Boot
0x00080000 U-Boot Environment

Kernel Loader sem U-Boot

O U-Boot é realmente uma ferramenta muito interessante, customizável e flexível, mas ainda temos a opção de carregar o kernel a partir da flash usando o programa de inicialização. Para isso vamos ter de fazer uma pequena alteração no programa de inicialização, por causa do endereço de carregamento e, principalmente, da quantidade de dados que serão lidos da flash para a RAM. Também é importante ter uma forma de passar parâmetros para o kernel, o que complica um pouco.

Parametros para o kernel

Existe um formato específico para a passagem de parâmetros de inicialização para o kernel através de tags e estruturas de dados binários. Entre as 10 tags existentes até esse momento, a mais interessante é a que passa a linha de comando. Entretanto, para garantir a integridade dos parâmetros, as tags que marcam o início e o fim dos parâmetros são obrigatórias. Em http://www.arm.linux.org.uk/developer/ pode-se encontrar um documento que descreve as tags. O kernel também é uma boa fonte de informações para estudos.

Um programa para formatação dos parâmetros

A ferramenta que grava dados na placa é o SAM-BA, que é capaz de executar scripts TCL. Em teoria seria possível fazer um script TCL para gerar os parâmetros em formato correto, mas como meus conhecimentos de TCL são nulos e meu foco não é aprender TCL nesse momento, construí um pequeno programa em C que recebe parâmetros pela linha de comando e gera um arquivo binário contendo os parâmetros no formato esperado pelo kernel.

Layout da flash

Para boot na NAND flash usando um sistema de arquivos também na flash, o layout será o seguinte:

Endereço Conteúdo
0x00000000 Bootloader
0x00020000 Kernel
0x00220000 Parâmetros
0x00400000 Sistema de Arquivos (root)

Para gravar essa estrutura na flash, use o script bootloader/scritps/direct_nand_nand.tcl .

Para boot na NAND flash usando um sistema de arquivos na NFS, o layout será o seguinte:

Endereço Conteúdo
0x00000000 Bootloader
0x00020000 Kernel
0x00220000 Parâmetros

Para gravar essa estrutura na flash, use o script bootloader/scritps/direct_nand_nfs.tcl .

Debug

Mensagens de debug nessa placa normalmente são enviadas pela porta DBGU. Para ativar o envio de mensagens pelo bootloader para a porta serial, defina a variável DEBUG=1 no script bootloader/1-bootstrap.sh.

Boot usando NFS

Para montar o sistema de arquivos usando NFS, defina as variáveis SERVERIP e DEVICEIP para os endereços IP do servidor (onde está o root do sistema de arquivos) e o endereço IP (fixo) do dispositivo. O script também cria um exemplo do arquivo de exportação de mountpoints no diretório build/tmp. Se usado como está, o endereço IP do servidor está definido como 192.168.20.1 e o do device é 192.168.20.100.

Melhorias

O boot demora menos de 20 segundos e, durante esse intervalo, nenhuma informação de progresso é mostrada para o usuário. Uma melhoria cosmética seria mostrar algo no display durante o boot.

Programas

01-alsa-utils.sh Utilitários ALSA
02-udev.sh Gerenciador de Dispositivos
03-bluez.sh Suporte para Bluetooth
04-bluez-tools.sh * Ferramentas de apoio para bluetooth
05-fscam.sh Captura Frames de Webcam
06-links.sh Browser gráfico simples
07-strace.sh * Utilitário strace
09-wirelesstools.sh * Ferramentas para wi-fi
10-wpa_supplicant.sh Suporte para Wi-Fi
12-fonts.sh Fontes genéricas
13-qingy.sh Programa de Logon gráfico

Os programas marcados com * teoricamente só são úteis durante o desenvolvimento.

Configurações

Configure suas redes Wi-Fi no arquivo app/files/wpa_list. O formato é simples: para cada linha, um nome de rede seguido pela senha, separados por '|' (pipe).
Configure os usuários pré-definidos no arquivo root/files/userlist.txt, cada usuário deve aparecer em uma linha com 3 campos separados por espaços, contendo pelo menos os 2 primeiros campos: NOME SENHA OUTROSDADOS

Bugs

Muitos. Os que eu achei:

    O driver ALSA de vez em quando não inicializa corretamente.
    O touchscreen precisa ser calibrado usando o programa ts_calibrate, copiei o arquivo de calibração gerado para minha placa de testes.
    O telnetd caiu algumas vezes sem motivo aparente.

Funciona/Não Funciona

U-Boot Bootloader
Boot Direto Bootloader
NAND Flash Hardware
USB Hardware
Ethernet Hardware
Touch Screen Hardware
RTC Hardware
Botões Alternativos Hardware
Sleep OS
Frame Buffer OS
Câmera USB (Webcam) OS
Wi-Fi OS
USB Flash Drive OS
Teclado USB OS
Mouse USB OS
Som ALSA Hardware
USB Device Hardware
CAN Hardware

Instalação

Veja instruções para obter os scripts no SourceForge, usando SVN.

Cruze os dedos e execute o script "armat91/all.sh" para fazer o download, compilar e instalar todos os programas, bibliotecas e acessórios necessários. O resultado dos scripts será colocado no arquivo armat91/buildlog.txt.

Divirta-se!

AT91SAM9263-EK durante o boot do Tabajara Linux com U-Boot

AT91SAM9263-EK durante o boot do Tabajara Linux com U-Boot

Atualizado com informações para obter os scripts via SourceForge.net.


AT91SAM9263-EK

Um amigo teve a coragem de me emprestar um AT91SAM9263-EK. É um kit de avaliação para o SoC AT91SAM9263, que é baseado no ARM926EJ-S, fabricado pela Atmel. Vem, junto com o kit, um CD com programas e manuais para utilização no Windows. Bom, acontece que eu uso Linux, então tive que pesquisar um pouco antes de poder usar o kit. Mas chega dessa sopa de letrinhas: ao invés de ficar toda hora escrevendo AT91SAM9263-EK, vou chamá-lo simplesmente de Frobo, Não, não tem nada a ver com o Tolkien.

Antes de continuar, leia a Limitação de Responsabilidade: saiba que as informações, os testes e procedimentos descritos nexte texto funcionaram na amostra que eu tenho em mãos, mas podem não funcionar para a sua amostra, podendo, até mesmo, danificá-la. Todas as informações estão disponíveis gratuitamente e de boa fé para fins didáticos, mas sem garantia nenhuma. A responsabilidade pela utilização das informações aqui contidas é inteiramente sua. Se não concordar com esses termos, não prossiga com a leitura.

A arquitetura do Frobo é muito simples: um processador com alguns periféricos. Tá, eu sei: essa descrição serve para qualquer computador! No caso do Frobo, as características mais interessantes são:

  • 64 MB de SDRAM
  • 256 MB de NAND Flash
  • 2 portas USB host
  • 1 porta USB device
  • 2 portas seriais (1 rs232 e 1 dbgu/rs232)
  • 1 porta ethernet
  • 1 módulo LCD 320x240 (1/4 VGA)
  • 1 entrada/saída áudio AC97
  • 2 LEDS programáveis

    Quem não gosta de samba...

    A Atmel fornece um programinha para você se comunicar com o Frobo, um tal de SAM-BA, no site tem o link para baixar e dançar, incluindo uma versão para Linux. Quando achei o link fiquei entusiasmado e já fui logo ligando tudo, queria carregar o Linux de referência, mas... acontece que o SAM-BA não funcionou no Ubuntu 10.10. Depois de descartar um problema na placa (usei a técnica da minha avó, doutora em física quântica: quanto a televisão dela, uma Colorado RQ a válvulas e com chassis de ferro parava de funcionar, ela dava uma forte pancada que, quase sempre, resolvia o problema), descobri que quando eu conectava o cabo USB do Frobo no laptop, o Linux estava carregando um driver chamdo cdc_acm, que não funciona. Alguém fez uma referência a isso (perdi o link) dizendo que o suporte ao CDC está quebrado desde o kernel 2.6.32 (ou algo assim). Então, para quem usa Ubuntu 10.10, pegue aqui os fontes do driver, coloque o driver cdc_acm na lista negra, recompile e instale, deve funcionar.
    Para colocar o driver cdc_acm na lista negra, crie um arquivo chamado blacklist-cdc.conf no diretório /etc/modprobe.d:

    # sudo sh -c "echo 'blacklist cdc_acm' >> /etc/modprobe.d/blacklist-cdc.conf"
    

    Isso vai evitar que o módulo seja carregado automaticamente quando o Linux detectar o dispositivo.

    Para compilar e instalar o novo driver, basta:

    # cd /tmp
    # wget cdcdriver.tgz
    # tar -zvxf http://william.net.br/files/cdcdriver.tgz
    # cd cdcdriver
    # make
    # sudo make install
    

    Mantenha esse módulo até que o driver oficial seja consertado, depois descarte-o. Quando o driver oficial estiver OK, não esqueça de removê-lo da lista negra:

    # sudo rm /etc/modprobe.d/blacklist-cdc.conf
    

    Se tudo estiver correto, você verá uma tela parecida com isso:

    /tmp/cddriver$ make
    make -C /lib/modules/2.6.35-23-generic/build M=/tmp/cdcdriver modules
    make[1]: Entering directory `/usr/src/linux-headers-2.6.35-23-generic'
      CC [M]  /tmp/cdcdriver/main.o
      LD [M]  /tmp/cdcdriver/cdc.o
      Building modules, stage 2.
      MODPOST 1 modules
      CC      /tmp/cdcdriver/cdc.mod.o
      LD [M]  /tmp/cdcdriver/cdc.ko
    make[1]: Leaving directory `/usr/src/linux-headers-2.6.35-23-generic'
    /tmp/cdcdriver$ sudo make install
    make -C /lib/modules/2.6.35-23-generic/build M=/tmp/cdcdriver modules_install
    make[1]: Entering directory `/usr/src/linux-headers-2.6.35-23-generic'
      INSTALL /tmp/cdcdriver/cdc.ko
      DEPMOD  2.6.35-23-generic
    make[1]: Leaving directory `/usr/src/linux-headers-2.6.35-23-generic'
    

    Agora conecte o cabo USB, da porta USB device para o seu computador. Remova o jumper J29 e ligue a placa. Execute o lsusb para saber se o device foi reconhecido:

    $ lsusb
    Bus 002 Device 003: ID 03eb:6124 Atmel Corp. at91sam SAMBA bootloader
    

    Para saber se o driver foi carregado, execute "lsmod | grep cdc":

    $ lsmod | grep cdc
    cdc                     3754  0
    usbserial              39507  1 cdc
    

    Por fim, vamos ver as mensagens do kernel usando "dmesg". Se as últimas linhas da saída forem parecidas com o que está abaixo, é um bom começo:

    # dmesg
    <------>
    [ 3047.158680] usb 2-1.1: new full speed USB device using ehci_hcd and address 5
    [ 3047.278721] USB Serial support registered for sam-ba
    [ 3047.279109] sam-ba 2-1.1:1.1: sam-ba converter detected
    [ 3047.279437] usb 2-1.1: sam-ba converter now attached to ttyUSB0
    [ 3047.279545] usbcore: registered new interface driver sam-ba
    [ 3047.279551] cdc: v1.0: Atmel SAM Boot Assistant (SAM-BA) driver
    

    Com isso, aprendemos que o módulo foi carregado e está acessível pela ttyUSB0. Agora é só sambar.

    Sam-ba

    Suponho que você já baixou o arquivo do site da Atmel, mas se não fez, faça-o agora. Salve e descompacte em algum lugar. O Sam-ba é um programinha meio besta, o ideal é que seja colocado em algum diretório que esteja no $PATH, minha experiência mostrou que para executar um script corretamente ele tem de ser executado a partir no mesmo diretório onde está o script. Por hora, vamos assumir que você extraiu o conteúdo do pacote do diretório sam-ba dentro do seu diretório $HOME, o executável vai ser chamado assim:

    $HOME/samba/sam-ba
    

    A tela inicial do sam-ba mostra a porta de comunicação e o modelo da placa, já que ele serve para outros modelos, não só para o Frobo. Normalmente tanto a porta como a placa serão reconhecidos e tudo o que precisamos fazer é clicar no botão "Connect".

    Depois de conectado, esta é a tela:

    Divirta-se.

    Veja também o texto sobre o Linux na at91sam9263-ek


  • Construindo um Sistema Linux - 3

    Nos episódios anteriores vimos como construir as ferramentas básicas para montar um sistema Linux. Apesar do foco nas ferramentas, montamos um disco virtual e testamos com o QEMU, mas o sistema construído não fazia grande coisa. Admito: não fazia nada. Então vamos ver o que precisamos ter para que o TBJ fique mais parecido com um Sistema Linux "normal", com um sistema de arquivos consistente, um sistema de inicialização, um shell, rede e, pasmem, um servidor telnet. Mas, antes de continuar, um aviso: Os sistemas/scripts construídos terão fins didáticos, embora a utilização para quaisquer outros fins seja livre, não haverá nenhuma garantia de funcionamento. A licença dos scripts é GPL3, a dos textos é FDL 1.3. As licenças dos programas usados é a que está publicada em cada um deles. Se usar qualquer parte deste texto como referência para cursos, palestras, projetos, trabalhos, outros artigos ou qualquer outra utilização legal, seja educado e mencione a fonte. Se possível, me avise. Os scripts funcionaram no Ubuntu 10.10.

    FHS

    Começando pelo sistema de arquivos, existe um padrão para os nomes dos diretórios e a localização dos binários, descrito no Filesystem Hierarchy Standard. Segue uma tabela resumida inspirada no artigo sobre FHS da Wikipedia, aqui em Inglês, mais completo e aqui, a especificação formal .

    / Raiz do sistema de arquivos.
    /bin/ Programas essenciais que precisam estar disponíveis em single user mode e para todos os usuários.
    /boot/ Arquivos necessários para o boot.
    /dev/ Devices
    /etc/ arquivos de configuração do sustema.
    /etc/opt/ arquivos de configuração para o /opt
    /etc/X11/ arquivos de configuração para o X
    /etc/sgml/ arquivos de configuração para SGML.
    /etc/xml/ arquivos de configuração para XML.
    /home/ Diretórios de usuários.
    /lib/ Bibliotecas essenciais para os programas que estão em /bin e /sbin.
    /media/ Diretório onde são montadas automaticamente as mídias removiveis (CD, DVD, Dispositivos de armazenamento USB)
    /mnt/ Sistemas de Arquivos montados temporariamente
    /opt/ Optional application software packages[12].
    /proc/ Pseudo-sistema de Arquivos procfs para acesso à estados internos do kernel e de drivers.
    /root/ Diretório do usuário root.
    /sbin/ Programas essenciais, normalmente sem acesso de usuários não-root.
    /srv/ Dados específicos.
    /tmp/ Arquivos temporários.
    /usr/ Localização secundária para dados somente-leitura para usuários. Contém a maioria dos utilitários e aplicativos de usuário.
    /usr/bin/ Aplicativos não essenciais (não necessários em single user mode) disponíveis para todos os usuários.
    /usr/include/ Arquivos de include para compilação de programas C.
    /usr/lib/ Bibliotecas usadas pelos programas em /usr/bin e /usr/sbin
    /usr/sbin/ Programas não essenciais (por exemplo, daemons de serviços de rede).
    /usr/share/ Dados compartilhados, não dependentes de arquitetura
    /usr/src/ Arquivos fonte (exemplo: fontes do kernel).
    /usr/X11R6/ X
    /usr/local/ Dados locais específicos para o host.
    /var/ Arquivos de conteúdo variável, que mudam durante a operação normal do sistema, como log, arquivos temporários, etc
    /var/cache/ Dados de cache dos aplicativos. Os aplicativos devem ser tolerantes ao apagamento dos arquivos contidos nesse diretório.
    /var/lib/ Dados persistentes modificados pelos programas quando em execução. Exemplo: banco de dados, dados dos instaladores, etc.
    /var/lock/ Arquivos de lock.
    /var/log/ Arquivos de log.
    /var/mail/ Arquivos de email dos usuários.
    /var/run/ Informações da sessão atual.
    /var/spool/ Arquivos aguardando para serem processados, como fila de impressão.
    /var/spool/mail/ Antiga localização dos correios de usuários.
    /var/tmp/ Arquivos temporários preservados após uma reinicialização.

    Scripts e Templates

    Nem todos os diretórios descritos serão usados, somente esses: bin, dev, etc, home, lib, mnt, proc, root, sbin, tmp, usr e var. Uma combinação de scripts e templates (em bom Português, modelo) será usada para gerar o sistema de arquivos:

    $DEVTOOLS/scripts/root/template Arquivos copiados diretamente para o novo sistema de arquivos, comum a todas as arquiteturas (devices, scripts de inicialização, etc)
    $DEVTOOLS/scripts/root Scripts para geração do sistema de arquivos, comuns a todas arquiteturas
    $DEVTOOLS/scripts/$ARCH/root Scripts para geração do sistema de arquivos, específicos para a arquitetura
    $DEVTOOLS/scripts/$ARCH/root/template Arquivos copiados diretamente para o novo sistema de arquivos, específicos de uma arquitetura

    Enfiar os arquivos dentro dos scripts deixaria os scripts gigantes e dificultaria a manutenção, por isso usaremos os templates. A organização dentro da template segue o mesmo padrão do sistema de arquivos, ou seja, o diretório da template é como se fosse o root do sistema de arquivos. Por exemplo, se quisermos colocar um arquivo "licença.txt" dentro do diretório "/etc", bastaria criar o arquivo "$DEVTOOLS/scripts/$ARCH/root/template/etc/licença.txt".

    Este esquema pode parecer meio exagerado, mas vai facilitar a vida quando for necessário lidar com mais de uma arquitetura/projeto.
    O funcionamento é bem simples: primeiro os scripts genéricos são executados, os arquivos das templates genéricas são copiados, e por último os scripts específicos da plataforma são executados, que podem fazer ajustes específicos para aquela plataforma. Assim, poderemos substituir um arquivo genérico por outro específico, se necessário.

    Busybox

    Um sistema que se preze tem de ter pelo menos um shell, senão qual a graça? Existem muitos shell disponíveis, alguns populares e poderosos (e grandes consumidores de recursos) e outros nem tanto. Bash é um exemplo de um shell "gordão". No TBJ usaremos o busybox, que é muito mais do que um shell: inclui mais de 300 ferramentas e utilitários comuns em um sistema Unix. É possível configurar quais ferramentas serão incluídas no busybox através de um menu parecido com o menu de configuração do kernel do Linux, mas compilaremos o busybox com todas as ferramentas habilitadas, usando o GCC construído para a plataforma selecionada, como neste script:

    cd ..
    . init.sh
    
    PATH=$PATH:$TBJ_ARCH_ROOT/bin
    TARBALL=busybox-1.18.0.tar.bz2
    URL=http://www.busybox.net/downloads/$TARBALL
    canonicalize ${TARBALL} BASENAME
    
    download $TARBALL $URL $TBJ_TAR
    extract $TBJ_SRC $TBJ_TAR/$TARBALL
    # prepara diretorio de build
    mkdir -p $TBJ_BUILD/busybox
    die_on_error "Erro criando diretorio de compilacao do busybox"
    # limpo eventuais tentativas antigas
    rm -fr $TBJ_BUILD/busybox/*
    cd $TBJ_SRC/$BASENAME
    # aplica patch
    download "busybox-1.18.0-buildsys.patch" "http://www.busybox.net/downloads/fixes-1.18.0/busybox-1.18.0-buildsys.patch" "$TBJ_SRC/$BASENAME"
    patch -N -p 1 < busybox-1.18.0-buildsys.patch
    # gera o .config
    make distclean
    make O=$TBJ_BUILD/busybox defconfig
    cd $TBJ_BUILD/busybox
    # conserta o prefixo
    sed -i -e "s@.*CONFIG_CROSS_COMPILER_PREFIX.*@CONFIG_CROSS_COMPILER_PREFIX=\"${TBJ_ARCH_TARGET}-\"@" .config
    # compila
    make
    die_on_error "Erro ao compilar o busybox" "Busybox Compilado"
    

    Init

    O busybox implementa um init (quase) compatível com SystemV, e vamos usá-lo. Apesar desse alguns considerarem esse estilo de init ultrapassado e pesado, é fácil de usar, compatível, bem documentado e servirá bem aos nossos propósitos. Como todos sabem, em um sistema sem RAM Disk (em inglês) inicial, o init é o primeiro processo a ser executado pelo kernel após a inicialização internal do kernel estar terminada. O kernel procura pelo executável do processo init na seguinte ordem: valor da variável "init=" passada na linha de comando do kernel, /sbin/init, /etc/init, /bin/init, /bin/sh, e executa o primeiro que encontrar. Aliás, é por isso que um sistema Linux que deixa qualquer usuário alterar a linha de comando do kernel no bootloader é completamente inseguro (basta adicionar "init=/bin/bash" à linha de comando do kernel, por exemplo, para acessar o sistema como root). Como o init é SystemV, usaremos o arquivo /etc/inittab. Esse arquivo contém dados sobre a composição inicial do sistema, como os terminais, os programas usados na ativação do terminal, o runlevel inicial e o que acontece quando há uma alteração de runlevel.

    # A base dsse script aparece no man do busybox
    # Esse script e sempre chamado, exceto quando em Single User Mode
    ::sysinit:/etc/rcS
    
    # tty1 a 4 iniciam o shell
    tty1::wait:/bin/sh
    #tty2::wait:/bin/sh
    #tty3::wait:/bin/sh
    #tty4::wait:/bin/sh
    
    # getty para tty5-6
    #tty5::respawn:/sbin/getty 38400 tty5
    #tty6::respawn:/sbin/getty 38400 tty6
    
    # Consoles nas portas seriais ttyS0/ttyS1
    #::respawn:/sbin/getty -L ttyS0 9600 vt100
    #::respawn:/sbin/getty -L ttyS1 9600 vt100
    #
    # Console num modem ligado na ttyS2
    #::respawn:/sbin/getty 57600 ttyS2
    
    # O que acontece quando ctrl+alt+del e pressionado
    ::ctrlaltdel:/bin/umount -a -r
    #::ctrlaltdel:/sbin/swapoff
    

    Um outro arquivo básico é o /etc/rcS, que aparece no inittab, sua função é fazer a inicialização do sistema. Como o init do busybox não suporta runlevels, usaremos esse script para executar todos os outros scripts de inicialização presentes no diretório /etc/rc.d, que começarem com 'S' , em ordem alfabética. Essa organização poderá ser usada mesmo que, no futuro, usemos outro init com suporte completo ao SystemV. Ah, no rcS também chamaremos o mount -a para preparar todos os dispositivos de armazenamento que aparecerem no fstab (mais sobre o fstab, abaixo).

    #!/bin/sh
    mount -a
    
    FILES=`ls /etc/rc.d/S*`
    for i in $FILES ; do
    	$i
    done
    

    Scripts de Inicialização

    Os scripts a seguir, colocados no diretório /etc/rc.d, serão executados em ordem crescente:

    Script Função
    S02modules Carrega os módulos do kernel necessários
    S03rwfs Monta áreas tmpfs em alguns diretórios
    S05syslog Inicia o daemon syslog
    S10network Inicia rede
    S90telnet Inicia daemon telnet
    S91gpm Inicia gerenciador de mouse
    S99local Scripts "de usuário" (aplicativos que não são de sistema). É um symlink para o /etc/rc.local

    Somente Leitura?

    O dispositivo armazenamento vai ser montado em modo somente-leitura (RO). Se houver necessidade, outras partições ou dispositivos de armazenamento poderão ser montado em outros diretórios, mas a base do sistema será somente-leitura, evitando alterações acidentais e que eventuais desligamentos/reset/crash resultem em dano ao sistema de arquivos. Um benefício adicional é que assim poderemos usar uma flash-rom como dispositivo de boot. Mas, como sempre, há um porém: alguns programas precisam escrever em arquivos, e esperam/assumem que certos diretórios permitem leitura e escrita (RW), /tmp, por exemplo. Por isso, vamos montar RAM disks em alguns diretórios, como /tmp e /var/tmp. Alguns arquivos que aparecem em diretórios somente leitura mas precisam permitir escrita serão links para diretórios RW. Os arquivos que precisam ser RW e tem conteúdo inicial, deverão ser links para o diretório /tmp/sys. O conteúdo inicial deverá ser colocado em /etc/template, durante a inicialização do sistema os arquivos que estiverem no diretório /etc/template serão copiados para o diretório RW /tmp/sys pelo script S03rwfs.

    Usuários

    Somente o usuário root está cadastrado, no entanto, pode ser desejável criar outros usuários durante a geração da imagem. Usaremos um programinha simples capaz de gerar o hash da senha:

    #include <stdlib.h>
    #include <stdio.h>
    #include <time.h>
    #define _XOPEN_SOURCE
    #include <unistd.h>
    #include <errno.h>
    
    const char salt_chars[] = "01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./";
    char *crypt(const char *key, const char *salt);
    
    static unsigned long long clock_ms(void)
    {
            struct timespec ts;
            clock_gettime(CLOCK_MONOTONIC, &ts);
            return ts.tv_sec * 1000ULL + ts.tv_nsec/1000000;
    }
    
    #define SALTKEY 8
    #define SALTEXTRA 5
    #define SALTSIZE (SALTKEY + SALTEXTRA)
    
    int main(int argc, char *argv[])
    {
            int result = 0;
            char *salt = NULL;
            char *enc = NULL;
            int i;
    
            if (argc < 2) {
                    printf("%s password\n", argv[0]);
                    return 1;
            }
            if (argc > 2) {
                    salt = strdup(argv[2]);
            } else {
                    srand(clock_ms());
                    salt = malloc(SALTSIZE);
                    salt[0] = 36;
                    salt[1] = 0x31;
                    salt[2] = salt[0];
                    for (i = 3; i < SALTSIZE - 2; i++)
                            salt[i] = salt_chars[rand() % (sizeof(salt_chars))];
                    salt[i++] = salt[0];
                    salt[i] = 0;
            }
            enc = crypt(argv[1], salt);
            if (enc) {
                    //printf("salt: %s encoded: %s\n", salt, enc);
                    printf("%s", enc);
            } else {
                    printf("Erro %d no crypt\n", errno);
                    result = 1;
            }
            return result;
    }
    

    Supondo que o fonte do arquivo foi salvo como "FILENAME.c", compile com "gcc -o tbpasswd -lrd -lcrypt FILENAME.c", o executável será chamado de tbpasswd. Este programa usa a função crypt, disponível na glibc. Usando esse programa, um script irá ler um arquivo chamado "userlist.txt", contendo a lista de usuários e senhas, e irá gerar o "/etc/shadow".

    fstab

    O fstab descreve os dispositivos de armazenamento que contém sistema de arquivos que deverão ser montados. Nosso fstab predefinido monta o dispositivo de boot, os pseudo-sistemas de arquivos (proc, sys, etc) e os diretórios de arquivos temporários.

    #
    LABEL=boot      /               ext2    noauto,defaults 1       1
    proc		/proc		proc	defaults	0	0
    sysfs		/sys		sysfs	defaults	0	0
    devpts		/dev/pts	devpts	gid=5,mode=620	0	0
    tmpfs		/dev/shm	tmpfs	defaults	0	0
    /dev/shm 	/tmp		tmpfs	defaults
    /dev/shm 	/var/log	tmpfs	defaults
    

    Rede

    Os scripts de controle e configuração de rede são bem simples. O script "/etc/rc.d/S10network" inicializará a rede, ativando as interfaces definidas no arquivo "/etc/network/interfaces". Se houver um arquivo "/etc/network/ipfixed", as variaveis contidas nesse arquivo serão usadas para configurar a interface, caso contrário o DHCP client daemon será iniciado para tentar configurar as interfaces de rede dinamicamentes, desde que haja um servidor DHCP disponível. Em qualquer caso, o script "/etc/network/config.sh" será chamado para configurar a(s) interface(s). O arquivo "/etc/network/hostname" conterá o nome do host.

    config.sh Script para configuração da interface
    hostname Nome do host (aceita FQDN)
    interfaces Lista de interfaces a serem configuradas
    ipfixed Configura uma interface com endereço IP fixo

    Scripts

    Um conjunto de scripts vai fazer tudo que foi descrito acima. Veja na primeira e na segunda partes como organizar o diretório dos fontes. O diretório de scripts foi dividido em sub-diretórios para facilitar a manutenção e separação:

    scripts
    	|
    	+ root -- diretório contendo scripts e templates genericos para geração do sistema de arquivos.
    	|	| Os scripts desse diretório não devem ser chamados diretamente.
    	|	|
    	|	+ template -- diretório com os templates genéricos.
    	|
    	+ ARCH -- diretório específico da arquitetura (exemplo: i586).
    		|
    		+ root -- diretório contendo scripts e templates específicos da arquitetura.
    		|	|
    		|	+ template -- templates específicos da arquitetura.
    		|
    		+ base -- diretório com scripts para geração das ferramentas básicas
    		|
    		+ toolchain - diretório com scripts para geração do binutils/gcc/libc
    

    Para montar o sistema, execute o script para construir o toolchain, entrando no diretório toolchain, o "maketoolchain.sh". Depois, execute o script das ferramentas básicas, entrando no diretório "base", o "makebase.h". Por último, para montar a imagem do disco, entre no diretório "root" da arquitetura e execute o script "makeroot.sh". Infelizmente essa última fase tem partes que precisam ser executadas como root, usando sudo, que vai pedir a senha. Se seu usuário de build não estiver na lista de sudoers, modifique seu sistema para incluí-lo na lista. Para testar, execute o "run.sh" dentro do diretório "root" da arquitetura. Esse script vai mapear a porta do telnet do cliente (o TBJ Linux) do QEMU para a porta 2300 do host, portanto, para acessar o sistema via telnet, execute:

    $ telnet 127.0.0.1 2300
    

    Se a porta 2300 do seu host estiver em uso, basta trocar por outra, editando o arquivo "run.sh". Não se esqueça de que os scripts tem de ser executados de dentro do diretório em que eles estão.

    Como Ficou

    Console SDL do QEMU

    Sessão Telnet

    Divirta-se.


    Construindo um Sistema Linux - 2

    Para continuarmos o desenvolvimento do Tabajara Linux, precisaremos de um ambiente de desenvolvimento estável, que possa ser facilmente replicado em outras máquinas e por outros desenvolvedores. Por isso construiremos o conjunto de ferramentas necessárias para a compilação de programas para nossa plataforma. Com esse conjunto de ferramentas teremos, ainda, a possibilidade de compilar uma plataforma de hardware diferente da nossa plataforma alvo, por exemplo, compilarmos em um PC para executar em um PowerPC ou ARM. Na parte 1 construímos um ambiente básico e uma estrutura de diretórios coerente, mas que terá de ser ligeiramente alterado para facilitar a convivência de diferentes plataformas na mesma árvore.
    Antes de continuar, um aviso importante: Os sistemas/scripts construídos terão fins didáticos, embora a utilização para quaisquer outros fins seja livre, não haverá nenhuma garantia de funcionamento. A licença dos scripts é GPL3, a dos textos é FDL 1.3. As licenças dos programas usados é a que está publicada em cada um deles. Se usar qualquer parte deste texto como referência para cursos, palestras, projetos, trabalhos, outros artigos ou qualquer outra utilização legal (nos 2 sentidos), seja educado e mencione a fonte. Se possível, me avise.
    Essa será a árvore de diretórios de desenvolvimento:

    ~/devtools +
               |
               + scripts - scripts para compilação
                    + <PLATAFORMA 1>
                    + <PLATADORMA 2>
                    ...
                    + <PLATAFORMA N>
               +
               |
               + tar - fontes compactados
               |
               <PLATAFORMA 1> (gerado a partir dos scripts)
                      |
                      + src - arquivos fonte das diversas ferramentas
                      |  + binutils-2.20
                      |  + gcc-4.4.5
                      |  + linux-2.6.36
                      |  + (?) outros fontes
                      |
                      |
                      + build - diretório de build
                      |  + gcc
                      |  + binutils
                      |  + libc
                      |  + linux - kernel
                      |  + ? build de outras ferramentas
                      |
                      + images - imagens de disco virtual
                      |
                      + mnt - temporário para montar discos virtuais
                      |
                      + tools - ferramentas auxiliares
                      |
                      + bin - programas compilados serão instalados aqui (gcc, binutils, etc)
                      |
                      + bin - diretório de instalação de executáveis (/bin)
                      + etc - diretório de instalação de arquivos de configuração
                      + include  - diretório de instalação de headers das libs, GCC, etc (depois de compilados)
                      + lib  - diretório de instalação de libs compiladas
                      + lib32  - diretório de instalação de libs compiladas, 32 bits (depende da plataforma)
                      + lib64  - diretório de instalação de libs compiladas, 64 bits (depende da plataforma)
                      + usr - diretório de instalação de libs, include e binários (/usr/bin)
                      |
                      + tmp - diretório temporário
               <PLATAFORMA 2> (gerado a partir dos scripts)
                      ...
               <PLATAFORMA N> (gerado a partir dos scripts)
    

    A configuração para compilação das ferramentas é umas das partes mais complicadas, já que são muitas opções de configuração, muitas delas mal explicadas ou muito complexas. Uma forma de começar é olhando os parâmetros de configuração usados em uma determinada distribuição (por exemplo: LFS, Debian, Ubuntu, Red Hat, OpenSUSE), alterando somente o que for necessário. O LFS é uma excelente fonte de informação, porque descreve passo a passo como fazer a compilação de várias ferramentas.

    Preparativos

    Para facilitar a vida e diminuir o risco de erros, scripts serão criados para automatizar a configuração e compilação dos vários programas e ferramentas. Várias operações são usadas repetidamente por vários scripts, então é melhor agrupá-las em um arquivo só, que vai servir como "include" para os scripts específicos das plataformas, que vão fazer a compilação de fato. O "common.sh" será usado para isso.

    # biblioteca de utilidades
    
    # testa o resultado do ultimo comando executado
    # e, se ocorreu um erro, aborta o script.
    # $1=mensagem de erro em caso de falha (OPCIONAL)
    # $2=mensagem de erro em caso de sucesso (OPCIONAL)
    function die_on_error
    {
    	local result=$?
    	local msgERR=$1
    	local msgOK=$2
    
    	if [ $result -ne 0 ] ; then
    		if [ -n "$msgERR" ] ; then
    			echo "$msgERR"
    		else
    			echo "ERR $result"
    		fi
    		exit $result
    	else
    		if [ -n "$msgOK" ] ; then echo "$msgOK" ; fi
    	fi
    }
    
    # Faz o download de um arquivo.
    # Se o arquivo ja existir, mostra uma mensagem e nao faz o download.
    # $1=nome do arquivo
    # $2=url para download
    # $3=diretorio para salvar o arquivo
    function download
    {
    	local FILE=$1
    	local URL=$2
    	local DIR=$3
    
    	if [ -e $DIR/$FILE ] ; then
    		echo "Arquivo ja existente, pulando download de $DIR/$FILE"
    	else
    		echo "Efetuando download de $FILE"
    		cd $DIR
    		wget -q $URL
    		die_on_error "Erro no download do arquivo $TARBALL para o diretorio $DIR (PAR=$PAR)" "Download OK!"
    	fi
    }
    
    # Extrai o nome de arquivo, removendo a extensao e o diretorio
    # $1 nome do arquivo
    # $2 nome da variavel para retorno
    function canonicalize
    {
    	local name=$1
    	local var=$2
    
    	name=`basename $name .gz`
    	name=`basename $name .bz2`
    	name=`basename $name .xz`
    	name=`basename $name .tar`
    	#name=`echo $name | sed -ne 's@\([^.]*\).*@\1@p'`
    	export $var="${name}"
    }
    
    # Procura uma substring em uma lista, desconsiderando diretorio e extensao
    # $1 lista (nao esqueca das aspas)
    # $2 substring a ser encontrada
    # $3 variavel que recebera o nome do item da lista
    function finddir
    {
    	local LIST=$1
    	local SUBSTRING=$2
    	local VAR=$3
    	local i
    	local f
    	local v
    
    	for i in $LIST ; do
    		canonicalize $i f
    		v=`echo $f | grep "$SUBSTRING"`
    		if [ -n "$v" ] ; then
    			export $VAR="${v}"
    			break;
    		fi
    	done
    }
    
    # Extrai um tarball.
    # $1=extrair para esse diretorio
    # $2=nome do arquivo (path completo, extensao, etc)
    function extract
    {
    	local DIR=$1
    	local TARBALL=$2
    	local PAR=
    	local f
    
    	case $TARBALL in
    	*.tar)
    		PAR="vxf"
    	;;
    	*.tar.gz|*.tgz)
    		PAR="zvxf"
    	;;
    	*.tar.bz2|*.bz2)
    		PAR="jvxf"
    	;;
    	* )
    		PAR="avxf"
    	;;
    	esac
    
    	canonicalize $TARBALL f
    	if [ -d "$DIR/$f" ] ; then
    		echo "Diretorio ja existente, pulando descompressao de $TARBALL"
    	else
    		echo "Extraindo $TARBALL para $DIR"
    		tar --keep-newer-files -C $DIR -$PAR $TARBALL &> /dev/null
    	fi
    	die_on_error "Erro ao descompactar arquivo $TARBALL para o diretorio $DIR (PAR=$PAR)" "Extracao OK!"
    }
    

    Cada arquitetura terá um diretório específico, com os scripts específicos daquela arquitetura. Dentro do diretório da arquitetura, teremos um arquivo que servirá para inicializar variáveis que servirão para guiar a compilação, o script "init.sh". A ideia é que pouca ou nenhuma manutenção seja necessária nos scripts, e, se tivermos de alterar algo, será no "init.sh". Três termos são recorrentes nas configurações do binutils, GCC e libc: target, host e build. Target é a plataforma na qual os programas vão gerar os outros binários. Host é a plataforma onde os binários serão executados e build é onde os fontes das ferramentas serão compilados (geralmente a compilação e configuração é feita na mesma máquina, e nesse caso não é necessário especificar). Os nomes usados para definir uma arquitetura são compostos por 3 ou 4 seções: cpu-arquitetura-OS-gnu. Exemplo: x86_64-pc-linux-gnu, i586-unknown-linux-gnu, i386-linux-gnu, etc. Como o nome da nossa distribuição é "tbj" e nossa primeira plataforma alvo é i586, o nome composto será i586-tbj-linux-gnu.

    EXPORT=export
    
    # CPU alvo
    $EXPORT TBJ_CPU_KERNEL=i386
    # CPU usada para montar o nome da arquitetura
    # Ocasionalmente pode ser diferente da $TBJ_CPU_KERNEL
    $EXPORT TBJ_CPU_ARCH=i586
    # Versao do kernel
    $EXPORT TBJ_KERNEL="2.6.36.1"
    # Para forcar a compilacao em 32 bits mesmo estando em 64
    $EXPORT TBJ_FORCE_32="-m32"
    # sufixo da arquitetura
    TBJ_SUFFIX=tbj-linux-gnu
    # nome da arquitetura alvo
    $EXPORT TBJ_ARCH_TARGET="${TBJ_CPU_ARCH}-${TBJ_SUFFIX}"
    # nome da arquitetura da maquina de build (CANDIDATO A REMOVER)
    $EXPORT TBJ_ARCH_BUILD="`uname -m`-${TBJ_SUFFIX}"
    # diretorio raiz da arvore de desenvolvimento
    $EXPORT TBJ_ROOT="`cd ../.. && pwd`"
    # diretorio raiz da arquitetura
    $EXPORT TBJ_ARCH_ROOT="$TBJ_ROOT/$TBJ_ARCH_TARGET"
    # onde estao os arquivos compactados (geralmente fontes compactados)
    $EXPORT TBJ_TAR="${TBJ_ROOT}/tar"
    # onde estao os fontes pronto para serem compilados
    $EXPORT TBJ_SRC="${TBJ_ARCH_ROOT}/src"
    # onde sera feita a compilacao
    $EXPORT TBJ_BUILD="${TBJ_ARCH_ROOT}/build"
    # diretorio de compilacao temporario (para resolver o problema libc/gcc)
    $EXPORT TBJ_STAGE="${TBJ_BUILD}/stage"
    # conta quantas CPUs existem
    CPUS=`grep processor /proc/cpuinfo | wc -l`
    # monta o numero de jobs do make baseado no numero de cpus logicas
    if [ -n "${CPUS}" ]; then
    	$EXPORT MAKE_JOBS="-j $((${CPUS} * 2))"
    else
    	$EXPORT MAKE_JOBS="-j 2"
    fi
    # criando os diretorios
    mkdir -p ${TBJ_TAR}
    mkdir -p ${TBJ_BUILD}
    mkdir -p ${TBJ_STAGE}
    mkdir -p ${TBJ_SRC}
    # forcamos a pesquisa usando $PATH (veja hash no $ man bash)
    set +h
    
    . ../common.sh
    

    Binutils

    O binutils contém um conjunto de ferramentas necessárias para o build, como por exemplo, o linker, o assember e outros programas. Nosso binutils terá que funcionar na máquina de desenvolvimento, mas gerará binários para a arquitetura alvo.

    #!/bin/bash
    
    . init.sh
    
    TARBALL=binutils-2.20.tar.bz2
    URL=ftp://sourceware.org/pub/binutils/releases/$TARBALL
    canonicalize ${TARBALL} BASENAME
    
    download $TARBALL $URL $TBJ_TAR
    extract $TBJ_SRC $TBJ_TAR/$TARBALL
    
    mkdir -p $TBJ_BUILD/binutils
    die_on_error "Erro criando diretorio de compilacao para binutils"
    # limpo eventuais tentativas antigas
    rm -fr $TBJ_BUILD/binutils/*
    cd $TBJ_BUILD/binutils
    CFLAGS=$TBJ_FORCE_32 CXXFLAGS=$TBJ_FORCE_32 $TBJ_SRC/$BASENAME/configure --prefix=$TBJ_ARCH_ROOT --target=$TBJ_ARCH_TARGET --with-sysroot=$TBJ_ARCH_ROOT
    
    die_on_error "Erro ao configurar binutils"
    
    make $MAKE_JOBS
    
    die_on_error "Erro ao compilar binutils"
    
    make install
    die_on_error "Erro ao instalar binutils"
    
    # criacao de links 32->64
    LIST=`find $TBJ_ARCH_ROOT/bin/ -type f -printf "%f "`
    cd $TBJ_ARCH_ROOT/bin
    for i in $LIST ; do
    	l=`echo $i | sed -ne 's@'$TBJ_ARCH_TARGET'-\(.*\).*@\1@p'`
    	if [ "$i" != "$l" ] ; then
    		ln -s $i $l
    	fi
    	#ln -s $i $TBJ_ARCH_TARGET-$l
    	#if [ "$TBJ_ARCH_TARGET" != "$TBJ_ARCH_BUILD" ] ; then
    	#	ln -s $i $TBJ_ARCH_BUILD-$l
    	#fi
    done
    
    echo "binutils final compilado e instalado"
    

    GCC Fase 1

    Compilar o GCC já é um pouco mais complicado, porque temos o problema do ovo e da galinha: o GCC precisa da libc, que precisa do GCC. A solução é fazer o build em 3 estágios: um GCC mínimo e provisório, que será usado para compilar uma libc provisória, que será usada para compilar o GCC final.

    GCC mínimo sem libc --> system headers + libc --> GCC final

    Precisaremos também dos fontes das bibliotecas obrigatórias para o build do GCC: MPC, GMP e MPFR. Em ftp://gcc.gnu.org/pub/gcc/infrastructure/, estão os fontes do MPC, GMP e MPFR adequados para o GCC, que usaremos no build. Escolhi as versões gcc-4.4.5.tar.bz2, binutils-2.20.tar.bz2, mpc-0.8.1.tar.gz, gmp-4.3.2.tar.bz2 e mpfr-2.4.2.tar.bz2.

    Os MPFR/GMP/MPC podem ser configurados e compilados separadamente ou terem os fontes colocados dentro da árvore dos fontes do GCC, que vai compilar e linkar com o GCC automaticamente. A desvantagem é que toda vez que recompilarmos o GCC as libs também serão recompiladas, mas esse será o modelo escolhido.

    Finalmente, configuraremos o GCC usando o mínimo possível em um diretório provisório, sem libs, somente linguagem C e com --with-newlib, que diz ao GCC para assumir que não existe uma libc presente. Claro, vamos usar o compilador existente, e nosso objetivo é executar na própria máquina.

    #!/bin/bash
    
    . init.sh
    
    #PATH=${TBJ_ARCH_ROOT}/bin:${PATH}
    URL=ftp://sourceware.org/pub/binutils/releases/$TARBALL
    DOWNLOAD_LIST="
    ftp://gcc.gnu.org/pub/gcc/releases/gcc-4.4.5/gcc-4.4.5.tar.bz2
    ftp://gcc.gnu.org/pub/gcc/infrastructure/mpc-0.8.1.tar.gz
    ftp://gcc.gnu.org/pub/gcc/infrastructure/gmp-4.3.2.tar.bz2
    ftp://gcc.gnu.org/pub/gcc/infrastructure/mpfr-2.4.2.tar.bz2
    "
    #GCCDIR=`echo $DOWNLOAD_LIST | sed -ne 's@.*\(gcc-[[:digit:].]\+\).t.*@\1@p'`
    
    # encontra os diretorios-base
    finddir "$DOWNLOAD_LIST" "gcc-" GCCDIR
    finddir "$DOWNLOAD_LIST" "mpfr-" MPFRDIR
    finddir "$DOWNLOAD_LIST" "gmp-" GMPDIR
    finddir "$DOWNLOAD_LIST" "mpc-" MPCDIR
    
    # prepara cada arquivo da lista
    for f in $DOWNLOAD_LIST ; do
    	TARBALL=`basename $f`
    	download $TARBALL $URL $TBJ_TAR
    	extract $TBJ_SRC $TBJ_TAR/$TARBALL
    done
    
    ln -s ../${MPFRDIR} $TBJ_SRC/$GCCDIR/mpfr
    ln -s ../${GMPDIR} $TBJ_SRC/$GCCDIR/gmp
    ln -s ../${MPCDIR} $TBJ_SRC/$GCCDIR/mpc
    
    # prepara diretorio de build
    mkdir -p $TBJ_STAGE/gcc
    die_on_error "Erro criando diretorio de compilacao para gcc"
    # limpo eventuais tentativas antigas
    rm -fr $TBJ_STAGE/gcc/*
    cd $TBJ_STAGE/gcc
    
    # configura
    CC="gcc $TBJ_FORCE_32" $TBJ_SRC/gcc-4.4.5/configure --prefix=$TBJ_STAGE --target=$TBJ_ARCH_TARGET --disable-nls --disable-shared --disable-decimal-float --disable-threads --disable-libmudflap --disable-libssp --disable-libgomp --enable-languages=c --without-ppl --without-cloog --disable-multilib --with-build-time-tools=$TBJ_ARCH_ROOT/bin
    
    die_on_error "Erro ao configurar gcc"
    
    # compila
    #NM_FOR_TARGET=nm AS_FOR_TARGET=as LD_FOR_TARGET=ld NM=nm AS=as LD=ld
    make $MAKE_JOBS
    
    die_on_error "Erro ao compilar gcc"
    
    # instala
    make install
    die_on_error "Erro ao instalar gcc"
    
    echo "gcc stage compilado e instalado"
    

    LibC

    É extremamente raro, mas não impossível, um sistema sem uma libc. A que está em uso na maioria dos Linux é a GNU C Library, ou glibc, ou libc6. Também existe a eglibc, uma libc desenvolvida a partir da glibc com a intenção de ser usada em sistemas embutidos, mas que também pode ser usada em sistemas clássicos. Outras libc estão disponíveis com graus variáveis de compatibilidade, como por exemplo a ucLibc, escolha a que for melhor para o seu projeto, glibc será despoticamente escolhida. Ela será compilada para ser executada na plataforma alvo, usando o GCC e binutils recém compilados.

    Antes de compilarmos a glibc precisaremos fazer alguns pequenos ajustes no GCC recém compilado. O GCC precisa saber onde encontrar arquivos de headers, bibliotecas, arquivos objeto de inicialização e finalização, ferramentas, etc, que estão configurados incorretamente em nosso novo GCC. Essas informações são definidas em tempo de compilação e estão gravadas no binário executável do GCC, mas podemos alterá-las usando um arquivo "specs" alternativo. Faremos um script, que pedirá ao GCC para mostrar as definições internas do "specs" e alterará somente o que nos interessa:

    #!/bin/bash
    
    . init.sh
    
    PATH=${TBJ_ARCH_ROOT}/bin:${TBJ_STAGE}/bin:${PATH}
    SPECS=`dirname $($TBJ_ARCH_TARGET-gcc -print-libgcc-file-name)`/specs
    rm $SPECS
    $TBJ_ARCH_TARGET-gcc -dumpspecs | sed \
    -e 's@/lib\(64\)\?/ld@'$TBJ_STAGE'/&@g' \
    -e "/^\*cpp:$/{n;s,$, -isystem $TBJ_ARCH_ROOT/usr/include,}" > $SPECS
    #-e 's@\([gS]\?crt[1in]\.o\)@'$TBJ_ARCH_ROOT'/usr/lib/\1@g' \
    echo "Novo spec: $SPECS"
    

    O script configure da glibc possui alguns testes que requerem a presença da libgcc_eh.a da arquitetura alvo. Não descobri outro meio de fazer o configure funcionar, a não ser usando uma libgcc_eh.a. Construiremos uma fajuta só para o configure ficar contente.

    #!/bin/bash
    
    . init.sh
    
    PATH=${TBJ_ARCH_ROOT}/bin:${TBJ_STAGE}/bin:${PATH}
    CC=${TBJ_ARCH_TARGET}-gcc
    CFLAGS=$TBJ_FORCE_32
    
    mkdir -p $TBJ_ARCH_ROOT/tmp
    cd $TBJ_ARCH_ROOT/tmp
    # Cria uma libgcc_eh.a de mentirinha, porque alguns testes feitos no configure
    # da libc precisam dela e vao falhar se ela nao existir.
    # Nao vai afetar o resultado final, o teste e que esta quebrado.
    echo '
    int fake(void)
    {
            return 0;
    } ' > fakegcc_eh.c
    $CC $CFLAGS -c -o fakegcc_eh.o fakegcc_eh.c
    die_on_error "Erro ao compilar fakegcc_eh.c"
    ar -r libgcc_eh.a fakegcc_eh.o
    die_on_error "Erro criar archive fakegcc_eh.a"
    ranlib libgcc_eh.a
    die_on_error "Erro criar criar indice em fakegcc_eh.a"
    cp libgcc_eh.a $TBJ_ARCH_ROOT/lib
    

    O último passo é gerar os headers de sistema. Os arquivos headers de sistema da arquitetura alvo são necessários para compilar a libc, eles são obtidos a partir do kernel.

    #!/bin/bash 
    
    . init.sh
    
    TARBALL="linux-2.6.36.1.tar.bz2"
    URL="ftp://kernel.org/pub/linux/kernel/v2.6/$TARBALL"
    canonicalize ${TARBALL} BASENAME
    
    download $TARBALL $URL $TBJ_TAR
    extract $TBJ_SRC $TBJ_TAR/$TARBALL
    
    cd $TBJ_SRC/$BASENAME
    CC="$TBJ_STAGE/bin/$TBJ_ARCH_TARGET-gcc $TBJ_FORCE_32" make ARCH=$TBJ_CPU_KERNEL INSTALL_HDR_PATH=$TBJ_ARCH_ROOT/usr headers_install
    
    die_on_error "Erro ao instalar headers de sistema"
    
    echo "Headers de sistema instalados"
    

    Agora sim estamos prontos para compilar a glibc.

    #!/bin/bash
    
    . init.sh
    
    PATH=${TBJ_ARCH_ROOT}/bin:${TBJ_STAGE}/bin:${PATH}
    TARBALL=glibc-2.12.1.tar.bz2
    URL=http://ftp.gnu.org/gnu/glibc/$TARBALL
    
    # encontra os diretorios-base
    finddir "$TARBALL" "glibc-" LIBCDIR
    download $TARBALL $URL $TBJ_TAR
    extract $TBJ_SRC $TBJ_TAR/$TARBALL
    
    # prepara diretorio de build
    mkdir -p $TBJ_BUILD/libc
    die_on_error "Erro criando diretorio de compilacao para libc"
    # limpo eventuais tentativas antigas
    rm -fr $TBJ_BUILD/libc/*
    cd $TBJ_BUILD/libc
    
    if [ "$TBJ_CPU_KERNEL" = "i386" ] ; then
    	# cria configparms (i386 para libc nao e suportado)
    	echo 'CFLAGS += -march=i486 -mtune=native' > configparms
    fi
    
    #LDFLAGS="-L$TBJ_STAGE/lib -L$TBJ_STAGE/lib32"
    $TBJ_SRC/$LIBCDIR/configure --build=$TBJ_ARCH_BUILD --host=$TBJ_ARCH_TARGET --prefix=/usr --with-headers=$TBJ_ARCH_ROOT/usr/include --disable-profile --without-gd --without-cvs --enable-addons --enable-kernel=2.6.36 libc_cv_forced_unwind=yes libc_cv_c_cleanup=yes --with-binutils=$TBJ_ARCH_ROOT/bin
    
    die_on_error "Erro ao configurar libc"
    
    make $MAKEJOBS
    
    die_on_error "Erro ao compilar libc"
    
    # instala no $TBJ_ARCH_ROOT
    make install install_root=$TBJ_ARCH_ROOT
    
    die_on_error "Erro ao instalar libc"
    
    echo "libc final compilado e instalado"
    

    GCC - Final

    Como vamos usar somente as linguagens C e C++, precisaremos especificar isso para o configurador, com isso o tempo de compilação e o espaço ocupado serão menores.

    #!/bin/bash
    
    . init.sh
    
    URL=ftp://sourceware.org/pub/binutils/releases/$TARBALL
    DOWNLOAD_LIST="
    ftp://gcc.gnu.org/pub/gcc/releases/gcc-4.4.5/gcc-4.4.5.tar.bz2
    ftp://gcc.gnu.org/pub/gcc/infrastructure/mpc-0.8.1.tar.gz
    ftp://gcc.gnu.org/pub/gcc/infrastructure/gmp-4.3.2.tar.bz2
    ftp://gcc.gnu.org/pub/gcc/infrastructure/mpfr-2.4.2.tar.bz2
    "
    # encontra os diretorios-base
    finddir "$DOWNLOAD_LIST" "gcc-" GCCDIR
    finddir "$DOWNLOAD_LIST" "mpfr-" MPFRDIR
    finddir "$DOWNLOAD_LIST" "gmp-" GMPDIR
    finddir "$DOWNLOAD_LIST" "mpc-" MPCDIR
    
    # prepara cada arquivo da lista
    for f in $DOWNLOAD_LIST ; do
    	TARBALL=`basename $f`
    	download $TARBALL $URL $TBJ_TAR
    	extract $TBJ_SRC $TBJ_TAR/$TARBALL
    done
    
    ln -s ../${MPFRDIR} $TBJ_SRC/$GCCDIR/mpfr
    ln -s ../${GMPDIR} $TBJ_SRC/$GCCDIR/gmp
    ln -s ../${MPCDIR} $TBJ_SRC/$GCCDIR/mpc
    
    # prepara diretorio de build
    mkdir -p $TBJ_BUILD/gcc
    die_on_error "Erro criando diretorio de compilacao para gcc"
    # limpo eventuais tentativas antigas
    rm -fr $TBJ_BUILD/gcc/*
    
    # se houver lib64 mas nao houver lib, cria um link
    cd $TBJ_ARCH_ROOT/usr
    if [ ! -d lib ] ; then
    	if [ -d lib64 ] ; then
    		ln -s lib64 lib
    	elif [ -d lib32 ] ; then
    		ln -s lib32 lib
    	fi
    fi
    
    # vai ao diretorio de build
    cd $TBJ_BUILD/gcc
    
    # configura
    # vai compilar na maquina atual, gerando binarios para a maquina $TBJ_ARCH_TARGET
    $TBJ_SRC/$GCCDIR/configure --prefix=$TBJ_ARCH_ROOT --with-sysroot=$TBJ_ARCH_ROOT --target=$TBJ_ARCH_TARGET --enable-languages=c,c++ --enable-threads=posix --with-arch-32=i586 --with-tune=generic --disable-libpchstdcxx --enable-shared --with-system-zlib --without-included-gettext --enable-nls --enable-clocale=gnu --enable-checking=release
    #--enable-targets=all
    
    die_on_error "Erro ao configurar gcc"
    
    # compila
    make $MAKE_JOBS
    
    die_on_error "Erro ao compilar gcc"
    
    # instala
    make install
    die_on_error "Erro ao instalar gcc"
    
    echo "gcc cross compilado e instalado"
    

    Kernel

    Usaremos "nosso" GCC recém construído para compilar nosso kernel. É necessário configurar o kernel antes de compilá-lo, o que pode ser feito entrando no diretório dos fontes do kernel e digitando:

    $ make ARCH="$TBJ_CPU_KERNEL" menuconfig
    

    Depois de configurar e salvar o config do kernel, copie para o mesmo diretório onde estão os scripts da arquitetura, usando o padrão kernelconfig.$TBJ_ARCH_TARGET, por exemplo, kernelconfig.i586-tbj-linux-gnu. Por último, limpe o estrago feito nos fontes do kernel, entrando no diretório onde estão os fontes do kernel e digitando:

    $ make mrproper
    

    Na parte 1 a configuraração do kernel está descrita com um pouco mais de detalhes.

    Depois de configurar o kernel, esse script ajudará na compilação:

    #!/bin/bash 
    
    . init.sh
    
    PATH=$PATH:$TBJ_ARCH_ROOT/bin
    TARBALL="linux-"$TBJ_KERNEL".tar.bz2"
    URL="ftp://kernel.org/pub/linux/kernel/v2.6/$TARBALL"
    canonicalize ${TARBALL} BASENAME
    
    download $TARBALL $URL $TBJ_TAR
    extract $TBJ_SRC $TBJ_TAR/$TARBALL
    
    # prepara diretorio de build
    mkdir -p $TBJ_BUILD/linux
    die_on_error "Erro criando diretorio de compilacao para o kernel"
    # limpo eventuais tentativas antigas
    rm -fr $TBJ_BUILD/linux/*
    # copia o .config
    cp kernelconfig.$TBJ_ARCH_TARGET $TBJ_BUILD/linux/.config
    cd $TBJ_SRC/$BASENAME
    
    make O=$TBJ_BUILD/linux CROSS_COMPILE=$TBJ_ARCH_TARGET- ARCH=$TBJ_CPU_KERNEL INSTALL_HDR_PATH=$TBJ_ARCH_ROOT/usr
    
    die_on_error "Erro ao compilar o kernel" "Kernel Compilado"
    
    make O=$TBJ_BUILD/linux CROSS_COMPILE=$TBJ_ARCH_TARGET- ARCH=$TBJ_CPU_KERNEL INSTALL_MOD_PATH=$TBJ_ARCH_ROOT/kernel INSTALL_MOD_STRIP=1 INSTALL_PATH=$TBJ_ARCH_ROOT/kernel modules_install install
    
    die_on_error "Erro ao instalar o kernel" "Kernel Instalado"
    

    Teste

    Para testarmos nosso novo kernel, recompilaremos o init usado na fase 1, agora usando nosso GCC e usando nossa glibc. Combinaremos tudo numa imagem e testaremos com o QEMU.

    #!/bin/bash 
    
    . init.sh
    
    PATH=$PATH:$TBJ_ARCH_ROOT/bin
    
    # prepara diretorio de build
    mkdir -p $TBJ_BUILD/init
    die_on_error "Erro criando diretorio de compilacao para o init"
    # limpo eventuais tentativas antigas
    rm -fr $TBJ_BUILD/init/*
    cd $TBJ_BUILD/init
    echo '
    #include  <stdio.h>
    #include  <time.h>
    
    extern char **environ;
    int main(int argc, char *argv[])
    {
    	struct tm tt;
    	time_t t;
    	int i;
    
    	for (i = 0; i < argc; i++) {
    		printf("arg[%d]=[%s]\n", i, argv[i]);
    	}
    	for (i = 0; environ[i]; i++) {
    		printf("env[%d]=[%s]\n", i, environ[i]);
    	}
    	while (1) {
    		t = time(NULL);
    		tt = *localtime(&t);
    		printf("Hello World %02d/%02d/%04d %02d:%02d:%02d\n",
    			tt.tm_mday, tt.tm_mon + 1, tt.tm_year + 1900,
    			tt.tm_hour, tt.tm_min, tt.tm_sec);
    		sleep(5);
    	}
    	return 0;
    }
    ' >> pminit.c
    
    $TBJ_ARCH_TARGET-gcc -Wl,-s -o init pminit.c
    
    die_on_error "Erro ao compilar o init" "Init compilado"
    

    Para verificar quais as libs necessárias para nosso init, vá até o diretório onde ele foi compilado e digite:

    $ ldd init
    	linux-gate.so.1 =>  (0xf77b7000)
    	libc.so.6 => /lib32/libc.so.6 (0x4fa2b000)
    	/lib/ld-linux.so.2 (0x4fa0b000)
    

    Cuidado ao interpretar a saida do ldd, ja que ele não sabe que vamos usar nossa própria libc, por isso vamos usá-lo somente como referência para pegar os nomes das libs que iremos copiar para o disco virtual, junto com a aplicação init, repetindo quase o mesmo que fizemos na primeira parte:

    #!/bin/bash 
    
    . init.sh
    
    PATH=$PATH:$TBJ_ARCH_ROOT/bin
    
    # prepara diretorio de build
    mkdir -p $TBJ_BUILD/init
    die_on_error "Erro criando diretorio de compilacao para o init"
    # limpo eventuais tentativas antigas
    rm -fr $TBJ_BUILD/init/*
    cd $TBJ_BUILD/init
    echo '
    #include  <stdio.h>
    #include  <time.h>
    
    extern char **environ;
    int main(int argc, char *argv[])
    {
    	struct tm tt;
    	time_t t;
    	int i;
    
    	for (i = 0; i < argc; i++) {
    		printf("arg[%d]=[%s]\n", i, argv[i]);
    	}
    	for (i = 0; environ[i]; i++) {
    		printf("env[%d]=[%s]\n", i, environ[i]);
    	}
    	while (1) {
    		t = time(NULL);
    		tt = *localtime(&t);
    		printf("Hello World %02d/%02d/%04d %02d:%02d:%02d\n",
    			tt.tm_mday, tt.tm_mon + 1, tt.tm_year + 1900,
    			tt.tm_hour, tt.tm_min, tt.tm_sec);
    		sleep(5);
    	}
    	return 0;
    }
    ' >> pminit.c
    
    $TBJ_ARCH_TARGET-gcc -Wl,-s -o init pminit.c
    
    die_on_error "Erro ao compilar o init" "Init compilado"
    
    # criamos os diretorios
    mkdir -p $TBJ_BUILD/{mnt,images}
    die_on_error "Erro ao criar diretorios"
    # criamos o disco
    dd if=/dev/zero of=$TBJ_BUILD/images/init.dsk count=32 bs=1M
    die_on_error "Erro ao criar disco"
    # formatamos o "disco":
    mkfs.ext2 $TBJ_BUILD/images/init.dsk
    die_on_error "Erro ao formatar disco"
    # montamos o disco para poder copiar nosso init (o mount tem que ser feito pelo usuário root)
    sudo mount $TBJ_BUILD/images/init.dsk $TBJ_BUILD/mnt -o loop
    die_on_error "Erro ao montar disco"
    # preparando os diretorios dentro da imagem
    mkdir $TBJ_BUILD/mnt/{sbin,lib}
    die_on_error "Erro ao criar diretorios dentro da imagem"
    # copiamos nosso init para dentro do disco virtual
    cp $TBJ_BUILD/init/init $TBJ_BUILD/mnt/sbin/init
    cp $TBJ_ARCH_ROOT/lib/{ld-linux.so.2,libc.so.6} $TBJ_BUILD/mnt/lib
    die_on_error "Erro ao copiar arquivos para o disco virtual"
    # desmontando
    sudo umount $TBJ_BUILD/mnt
    

    É, não copiamos a linux-gate.so.1. Ela não existe, não se preocupe.
    Pode parecer que o resultado não foi muito diferente do que fizemos na primeira parte... mas foi! Agora fizemos usando o "nosso" GCC e a "nossa" libc. Além disso, o init ficou bem menor: 500 KB contra menos de 6 KB. Essa infraestrutura servirá de base para o restante do sistema. Finalmente, para testar, use o seguinte script:

    #!/bin/bash
    
    . init.sh
    
    qemu -kernel $TBJ_ARCH_ROOT/kernel/vmlinuz-${TBJ_KERNEL}emb -no-kvm -append "root=/dev/sda" $TBJ_BUILD/images/init.dsk
    

    Lista de Scripts

    Clique para baixar os scripts

    . Descompacte o arquivo dentro do diretório das ferramentas de desenvolvimento, seguindo a hierarquia de diretórios vista acima.

    1-binutilscross.sh compilar binutils
    2-gccstage.sh compilar GCC, primeiro estágio
    3-fixspecsstage.sh Ajusta specs do GCC
    4-preparegccstage.sh Cria uma library para poder configurar a glibc
    5-sysheaders.sh Instala arquivos de headers de sistema
    6-libc.sh Compila glibc
    7-gcccross.sh Compila GCC final
    8-kernel.sh Compila kernel usando o GCC cross
    9-testinit.sh Cria um init de teste e monta um disco virtual
    doall.sh Executa todos os scripts na ordem correta
    run.sh Executa o QEMU para testar
    init.sh Inicializa as variáveis de configuração (não usado diretamente)
    kernelconfig.i586-tbj-linux-gnu config do kernel

    Como ficou


    Construindo um Sistema Linux - 1

    Para quem está estudando o funcionamento do Linux e não quer ser "atrapalhado" pela distribuição, para aqueles que não querem e para aqueles não podem usar uma das diversas distribuições Linux disponíveis, existe a opção de montar seu próprio OS. Como todos os componentes básicos do Linux estão disponíveis para download, incluindo os códigos fonte, é relativamente simples montar tudo. Existe até uma "distribuição" feita assim: LFS (Linux from Scratch), contendo instruções detalhadas de como fazer para construir seu próprio sistema, a partir do zero (ou quase).
    Minha intenção é fazer tudo de forma ainda mais simples do que o descrito no LFS, e facilmente adaptável para outras arquiteturas de hardware diferentes do PC. E, é claro, em português.
    Por último, um aviso: Os sistemas construídos terão fins didáticos, embora a utilização para quaisquer outros fins seja livre, não haverá nenhuma garantia de funcionamento. A licença dos scripts é GPL3, a dos textos é FDL 1.3. As licenças dos programas usados é a que está publicada em cada um deles. Se usar qualquer parte deste texto como referência para cursos, palestras, projetos, trabalhos, outros artigos ou qualquer outra utilização legal, seja educado e mencione a fonte. Se possível, me avise.

    Decisões de projeto

    Já sabemos que o kernel será o Linux, mas temos que escolher o hardware em que ele vai ser executado. Vou optar pelo mais comum:

    • PC compatível
    • CPU Intel i586
    • RAM min 8 MB, max 64 MB, dependendo dos aplicativos
    • HD min 4 MB, máximo 128 MB, dependendo dos aplicativos
    • Porta Serial
    • SVGA
    • USB
    • Teclado, Mouse (alguns casos)

    Ambiente de desenvolvimento

    Todas (ou quase todos) os aplicativos, ferramentas e até o mesmo o kernel do Linux são compilados pelo GCC, por isso ele é obrigatório. É importante escolher uma versão que funcione corretamente com o kernel desejado e com as versões dos aplicativos do Sistema Operacional. Geralmente as versões mais novas do GCC funcionam com os fontes mais recentes dos aplicativos, mas às vezes ocorrem problemas. Fica mais fácil de escolher se olharmos para a versão em uso pelas distribuições mais recentes, por exemplo, Ubuntu 10.10. Eles provavelmente tiveram que compilar todas os aplicativos que vão rodar no nosso SO, e, se funcionou para eles, também deverá funcionar aqui. O GCC do 10.10 é o 4.4.5. O GCC precisa do binutils para diversas tarefas, como o linker, e o binutils em uso no Ubuntu 10.10 é o 2.20. Outra ferramenta essencial é o GNU make, para processar os Makefiles toda aplicação, incluindo o kernel, tem. Outras ferramentas são o automake, autoconf, awk.
    O ponto é que precisamos de um ambiente de desenvolvimento estável, que não fique sujeito às mudanças e atualizações do seu sistema de desenvolvimento, e que possa gerar os mesmos resultados, seja numa máquina Red Hat 4, Ubuntu 8.04 ou Ubuntu 10.10. Podemos atingir esse objetivo escolhendo uma distribuição e qualquer e obrigando todos os desenvolvedores a utilizarem sempre a mesma versão quando estiverem compilando/recompilando os aplicativos/kernel do nosso sistema operacional, por exemplo, através de uma máquina exclusivamente para compilação. Teremos também que controlar as atualizações, para evitar o build deixe de funcionar ou, pior, que gere resultados incorretos, para os aplicativos no sistema operacional. Uma outra forma de manter o ambiente de desenvolvimento estável é criar um repositório com as ferramentas usadas, e compilar para que funcione nos sistemas operacionais que desejarmos, até mesmo com arquiteturas de hardware diferentes. Essa última opção dá um pouco mais de trabalho, mas o ambiente de desenvolvimento ficará bem mais estável e confiável, sem depender da distribuição escolhida pelo desenvolvedor. Como a primeira opção é fácil de usar, bastando instalar os pacotes necessários, vamos ver como construir o ambiente usando a segunda opção.
    As instruções e scripts foram testados no Ubuntu 10.04, mas devem funcionar em qualquer distribuição recente (CentOS 5/6, RHEL5/6, SLES11, Ubuntu 9/10.xx). Certifique-se de que estão instalados o GCC, o LD e o GNUMake.

    Diretórios

    Precisamos definir uma estrutura de diretório onde serão instalados as ferramentas, aplicativos binutils, kernels, bibliotecas, os fontes, etc. Essa é uma das coisas que, se começarem erradas, vão nos perseguir pelo resto da vida. Para facilitar, vamos deixar tudo dentro de um mesmo diretório: vou criar o diretório "devtools" dentro do meu diretório home ($HOME): /home/william/devtools. Esse será o diretório raiz das ferramentas, vamos apelidá-lo de $DEVTOOLS. Uma dica é usar, sempre que possível, paths relativos ou formados a partir de variáveis de ambiente, para que o ambiente de desenvolvimento funcione para qualquer usuário. Dentro desse diretório, criaremos diversos outros diretórios, a árvore vai ficar parecida com essa:

    ~/devtools +
               |
               + src - arquivos fonte das diversas ferramentas
               |  + scripts - scripts usados para o build
               |  + binutils-2.20
               |  + gcc-4.4.5
               |  + linux-2.6.36
               |  + (?) outros fontes
               |
               |
               + build - diretório de build
               |  + gcc
               |  + binutils
               |  + libc
               |  + ? build de outras ferramentas
               |
               + images - imagens de disco virtual
               |
               + mnt - temporário para montar discos
               |
               + tar - fontes compactados
               |
               + tools - ferramentas auxiliares
               |
               + bin - programas compilados serão instalados aqui (gcc, binutils, etc)
               |
               + include  - headers das libs, GCC, etc (depois de compilados)
               |
               + lib  - libs compiladas
               |
               - lib32 - libs versão 32 bits
    

    Como o diretório devtools vai ser usado frequentemente, defina uma variável de ambiente para esse diretório quando iniciar uma sessão do shell:

    $ export DEVTOOLS="$HOME/devtools"

    Também é possível definir essa variável automaticamente toda vez que se iniciar uma sessão do shell. Para o bash, edite o arquivo ~/.bashrc e adicione a linha abaixo:

    export DEVTOOS=$HOME/devtools

    Um aviso importante é que, a não ser que seja explicitamente indicado, NÃO execute nenhum comandos ou scripts como root (ou usuário com poderes de root), para evitar que os programas já instalados sejam destruídos por engano.

    Composição do Sistema

    Inicialmente vamos montar um sistema mínimo. Mas o que é necessário para um sistema Linux mínimo?
    - kernel: claro, é indispensável.
    - boot loader: o kernel precisa ser carregado em memória e colocado em execução, trabalho para um bootloader como LiLO, GRUB, LoadLin, etc.
    - init: esse é o primeiro aplicativo executado automaticamente pelo kernel logo após a inicialização do kernel.

    Kernel

    Escolhi o kernel 2.6.36 porque é o mais recente enquanto escrevo. Faça o download a partir do http://kernel.org, salve no diretório $DEVTOOLS/tar e extraia o arquivo compactado para o diretório $DEVTOOLS/src.

    $ mkdir -p ${DEVTOOLS}/{tar,src}
    $ cd ${DEVTOOLS}/tar
    $ wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.36.tar.bz2
    $ tar -C ${DEVTOOLS}/src -jvxf ${DEVTOOLS}/tar/linux-2.6.36.tar.bz2
    

    É necessário configurar o kernel antes de compilá-lo, entramos no diretório $DEVTOOLS/src/linux-2.6.36. Esse é um assunto extenso demais para caber aqui, por isso recomendo usar o .config fornecido, salvando o arquivo como ".config" dentro do diretório dos fontes do kernel. Se quiser configurar o kernel ou se a configuração fornecida não atender seus objetivos, siga os passos abaixo:

    1. Configuramos para nenhum módulo (se preferir, use o .config fornecido como base):

    $ cd $DEVTOOLS/src/linux-2.6.36
    $ make allnoconfig

    2. Entramos no menu de configuração:

    $ make menuconfig

    ou

    $ make gconfig

    2. Selecionamos CPU "Pentium-III/Celeron(Coppermine)/Pentium-III Xeon"
    Sua configuração deve ter, pelo menos:

    CONFIG_PCI=y
    CONFIG_EXT2_FS=y
    CONFIG_PROC_FS=y
    CONFIG_SYSFS=y
    CONFIG_TMPFS=y
    

    Se o hardware usado tiver USB, som, vídeo, porta serial, porta paralela, etc, certifique-se de adicionar o suporte correspondente.

    Uma vez configurado, compilamos, nesta fase usaremos o compilador instalado em sua máquina:

    $ make ARCH=i386 CC="gcc -m32"

    E depois, instalamos:

    $ make ARCH=i386 INSTALL_MOD_PATH=$DEVTOOLS/kernel INSTALL_MOD_STRIP=1 INSTALL_PATH=$DEVTOOLS/kernel modules_install install

    Init

    Um init SystemV tradicional é capaz de lidar com runlevels, inittab, terminais, etc. Nosso init vai ser bem mais simples: vai ser a única aplicação do sistema, com um único usuário e nada mais para ser feito. Claro que um sistema como esse não é muito flexível e não permite muita coisa além de mostrar um "hello world", mas é interessante para fins didáticos ou se o seu sistema tiver uma restrição muito séria de recursos. Nosso init vai apenas mostrar as variáveis de ambiente e os argumentos da "linha de comando" passados pelo kernel.

    #include  <stdio.h>
    #include  <time.h>
    
    extern char **environ;
    int main(int argc, char *argv[])
    {
            struct tm tt;
            time_t t;
            int i;
    
            for (i = 0; i < argc; i++) {
        printf("arg[%d]=[%s]\n", i, argv[i]);
            }
            for (i = 0; environ[i]; i++) {
        printf("env[%d]=[%s]\n", i, environ[i]);
            }
            while (1) {
        t = time(NULL);
        tt = *localtime(&t);
        printf("Hello World %02d/%02d/%04d %02d:%02d:%02d\n",
    			tt.tm_mday, tt.tm_mon + 1, tt.tm_year + 1900,
    			tt.tm_hour, tt.tm_min, tt.tm_sec);
        sleep(5);
            }
            return 0;
    }
    

    Por enquanto não queremos depender da libc ou qualquer outra biblioteca carregada dinamicamente em nosso init, por isso vamos compilá-lo estaticamente. Também não vamos debugar, por isso vamos remover todos os símbolos com strip-all. O m32 serve para dizer ao compilador que queremos um binário de 32 bits, mesmo que o compilador seja 64 bits.
    Se você salvar o arquivo como main.c, compile com

    $ gcc -m32 -Wl,--strip-all -static -o init main.c

    Se preferir pegue tudo pronto aqui. É só descompactar, entrar no diretório dos fontes e digitar

    $ make

    Agora só falta a parte fácil: colocar tudo isso junto... Para nossos testes, vamos montar um máquina virtual para ser executada com o qemu. O sistema de arquivos será o ext2. Inicialmente não teremos um boot loader, já que o QEMU tem a capacidade de carregar o kernel.

    Vamos criar nosso "disco" com 32 megabytes dentro do diretório "images", usando dd:

    $ dd if=/dev/zero of=${DEVTOOLS}/images/init0.dsk count=32 bs=1M

    Agora formatamos o "disco":

    $ mkfs.ext2 ${DEVTOOLS}/images/init0.dsk

    Montamos o disco para poder copiar nosso init (o mount tem que ser feito pelo usuário root):

    $ mkdir -p ${DEVTOOLS}/mnt
    $ sudo mount ${DEVTOOLS}/images/init0.dsk ${DEVTOOLS}/mnt -o loop

    Copiamos nosso init para dentro do disco virtual:

    $ cp ${DEVTOOLS}/src/pminit/init ${DEVTOOLS}/mnt/init

    Não vamos colocar mais nada no disco por enquanto, nem os módulos dinâmicos do kernel já que o ext2 e o vídeo estão compilados como estáticos. Então, já podemos desmontar o disco:

    $ sudo umount $DEVTOOLS/mnt

    Finalmente podemos iniciar a máquina virtual:

    $ qemu -kernel ${DEVTOOLS}/kernel/vmlinuz-2.6.36emb -append "root=/dev/sda init=/init" ${DEVTOOLS}/images/init0.dsk

    Pronto, agora você já pode impressionar seus amigos mostrando o seu próprio Linux. Provavelmente eles vão te perguntar "Mas ele serve para o quê mesmo?", mas não se abale, toda caminhada tem de começar pelo primeiro passo.


    Sony Vaio F111 Review (VPCF111FB)

    Hardware

    • Modelo VPCF111-FB
    • Display LCD FullHD 1920x1080.
    • Câmera 640x480 em cima da tela.
    • 3 portas USB (1 delas é compartilhada com e-SATA).
    • 1 porta e-SATA (compartilhada com 1 USB - ou usa um ou o outro).
    • Express card.
    • Porta de rede.
    • Saída VGA.
    • Saída HDMI.
    • Porta S400 (FireWire 400 Mbits/s).
    • Chave para desligar rádios (wifi/bluetooth).
    • Touchpad.
    • Saída fone de ouvido.
    • Entrada Microfone
    • Saída áudio óptica (compartilhada com saida para fone de ouvido).
    • Entrada para cartões MagicGate
    • Entrada para cartões SD
    • LED's NumLock/CapsLock/System Lock/Power (em cima do teclado)
    • LED's HD, Posição da chave Wireless (rádios), Status da Bateria (na frente).
    • LED de acionamente da câmera.
    • Teclas especiais para desligar o display, iniciar/parar player.
    • Placa de vídeo Nvidia G310M.

    Pontos positivos

    • Tamanho e resolução do display
    • Velocidade da CPU
    • Quantidade de memória
    • Pouco aquecimento
    • Teclado agradável
    • Teclado não esquenta

    Pontos negativos

    • Peso
    • Tamanho da font
    • Com tanto espaço, a tecla Barra e Interrogação só podem ser usadas com AltGR.
    • As teclas poderiam ser um pouquinho maiores (há espaço).
    • Manual fraquíssimo. Nem pode ser chamado de manual. Nem explica qual tecla entra na configuração da BIOS.
    • Os sons escandalosos do Windows 7 em resposta à ações (é só desabilitar, gostaria que viesse assim).
    • Duração da bateria (2 horas)
    • Reconhecimento de fala do Windows 7 não funciona (diz que não está disponível para o idioma atual).
    • O pino da tomada está no novo padrão brasileiro, aquele que não encaixa em tomada nenhuma. Coisa da ABNT. Compre uma régua compatível.
    • A licença do Windows Live. Medo, muito medo.

    Esquisitices

    • Não vem nenhum CD/DVD. Existe uma partição de restauração, possivelmente o Windows todo está lá. Não a remova.
    • Existe uma enorme área sem uso na parte de cima do teclado.
    • Demorei para encontrar onde habilitar dispositivos bluetooth, então vai a dica: Painel de controle, Dispositivos e Impressoras, Adicionar um dispositivo.
    • Demorei um pouco para acostumar com o touchpad (mouse) fora de centro.

    Windows 7

    Funcionou de cara

    • Câmera embutida
    • Bluetooth
    • Fone Nokia N82
    • Headset Nokia
    • USB
    • Porta HDMI em FullHD com Painel LG 47
    • Dual-Head (monitor nativo + monitor FullHD via HDMI)
      • Funcionou tanto na opção CLONE -- quando os dois monitores mostram a mesma imagem-- como na opção COMPOSTO, ou seja, os 2 monitores mostrando imagens independentes.
    • Rede (Roteador DI-524)
    • Rede WiFi (com WPA2)
    • Reprodução Blu-Ray
    • Porta VGA com Monitor LG 22
    • Impressora doméstica em rede com CUPS.

    Funcionou após fazer download

    • Laserjet 1020 (para rede também precisa do driver, que não está no conjunto de drivers fornecidos).
    • Scanner Plustek OpticFilm 7300 - O driver antigo nem instala
    • Nokia PC suite

    Outros programas alternativos  que funcionaram (eu sei, não depende do hardware, mas só para informar)

    • BrOffice
    • PDFCreator
    • Pidgin
    • Firefox

    Não funcionou

    • Captura de TV, a PixelView ainda não tem uma versão do driver para Win7.

    Linux (Ubuntu 9.10)

    Atenção: estou apenas descrevendo o procedimento que eu executei em minha máquina, não há garantia de que isso vá funcionar ou mesmo danificar sua máquina. Se quiser tentar, será por sua conta e risco.

    Na instalação original, o Windows ocupa o disco todo. Para instalar o Linux, a primeira coisa a fazer é diminuir a partição do Windows: vá em Painel de Controle, Ferramentas Administrativas, Gerenciamento do Computador.

    No painel esquerdo, selecione Repositório/Gerenciamento de Disco. No Disco 0, clique na partição C: com o botão direito, e escolha a opção Diminuir Volume. Digite o tamanho desejado na caixa Digite o Espaço a Diminuir em MB. O processo vai demorar um pouco para ser concluído.

    Um outro problema é que o HD é o dispositivo padrão de boot, como instalei de um CD, tive que alterar a ordem dos dispositivos de boot. Para isso, durante a inicialização, pressione F2.

    Vá na página de dispositivos de boot e altere a ordem para CD em primeiro, HD em segundo. Agora é só seguir os procedimentos de instalação, normalmente.

    Funcionou de cara

    • Wifi (A/B/G)
    • Bluetooth (incluindo perfil estéreo, perfil com microfone, celular)
    • Câmera
    • Ethernet
    • As 3 USBs
    • Saída VGA
    • Saída HDMI
    • Botões multimídia
    • Leitor CD/DVD
    • Gravador DVD
    • Monitor de bateria
    • Gerenciador de velocidade de CPU
    • Leitor SD card (10 MB/s)
    • Saída de fone
    • Entrada de microfone
    • Microfone embutido.

    Não funcionou

    • O driver de vídeo nativo Nvidia não funcionou. O primeiro problema foi não reconhecer o próprio painel LCD! Consegui consertar isso alterando pegando o EDID (informações do monitor) no Windows e configurei o xorg.conf para usar o EDID lido. Apesar de aparecer imagens, não foi possĩvel ativar o desktop 3D. Além disso ficou instável, travou algumas vezes. (CONSERTADO). Após o conserto ativei o Desktop 3D Compiz Fusion, ficou muito bom. Adicione o Cairo Dock, versão OpenGL, o desktop fica muito bonito e o gasto extra de CPU é insignificante.
    • O som usando os falantes embutidos não funcionou (apesar da saída de fone de ouvido funcionar normalmente) (CONSERTADO).
    • As teclas ASSIST e VAIO não são reconhecidas (CONSERTADO).
    • O Blu-Ray player não funcionou no Linux. Tentei algumas gambiarras garimpadas na Internet, nenhuma deu certo.
    • Não consegui ler memory stick.
    • O ajuste de brilho da luz de fundo do display não funciona, fica sempre no máximo (CONSERTADO). Depois de consertado, a duração da bateria melhorou consideravelmente.
    • O sensor de luminosidade (uma bolinha acima da tecla F9) não funciona.
    • Dual Head.

    Funcionou parcialmente

    • A função de rolar do touchpad não funcionou corretamente, parece que o touchpad é reconhecido como mouse PS∕2. A movimentação do ponteiro funcionou OK (apesar de a tela FullHD ser muito grande para ficar deslocando o ponteiro).

    Não foi testado

    • saída eSATA
    • saída i400
    • ExpressCard

    Fotos


    Debug - User Mode - 2 (GDB)

    GDB (GNU Debugger) é um programa que ajuda a saber o que está acontecendo dentro de um outro processo (debuggee), acompanhar sua execução, parar a execução, ver e alterar seu estado interno. Resumindo: é um debugger :) . O GDB foi feito primariamente para debugar programas escritos em C/C++, mas possui suporte de nível variado para programas escritos em outras linguagens. O GDB funciona em vários Sistemas Operacionais e em várias arquiteturas de processadores, mas neste texto vou considerar o GDB debugando programas em C, executando em Linux, arquitetura x86. Não tenho aqui a pretensão de apresentar um guia abrangente e cobrir cada uma das peculiaridades e detalhes obscuros do GDB nem seu funcionamento interno, ao contrário, a intenção é mostrar como fazer o básico, através de exemplos práticos. Muitos dos resultados mostrados podem ser obtidos de formas diferentes da mostrada, ficando a escolha ao gosto do freguês. Por fim, os melhores lugares para encontrar informações detalhadas sobre todos os aspectos do GDB são:

  • # info gdb
  • # man gdb
  • Página do GDB.
  • Como qualquer debugger decente, o GDB possui as seguintes características:

  • Executar um programa, parando na primeira instrução ou linha de código.
  • Executar um programa passo a passo.
  • Criar e gerenciar breakpoints, inclusive breakpoints de expressões e breakpoints condicionais.
  • Alterar o estado da CPU, registradores, pilha.
  • Examinar e alterar o valor de variáveis, salvar trechos de memória para um arquivo.
  • Parar o programa em caso de erro (exceção).
  • Criar tracepoints
  • Atachar o debugger à um programa em execução.
  • Gerar/Debugar core dumps.
  • Criar scripts de debug usando Python.
  • Debugar um programa executando em um outro computador.
  • Des-debugar (é, eu inventei a palavra).
  • Gravar e reproduzir uma sessão.
  • Antes que seja tarde, um pequeno glossário para esclarecer alguns termos:

    Debugger Um programa auxiliar capaz de examinar/alterar o estado interno de um outro programa.
    Debuggee O programa que está sendo debugado pelo Debugger.
    GNU GNU is Not Unix.
    GDB GNU Debugger, o programa em estudo
    GCC GNU Compiler Collection, o compilador C/C++ usado nos exemplos
    ELF Executable and Linkable Format, uma especificação de formato para arquivos binários executáveis e bibliotecas. Padrão no Linux.
    COFF Common Object File Format, uma especificação de formato de arquivos binários executáveis e bibliotecas. Perdeu espaço no mundo Unix para o ELF, mais completo.
    DWARF Formato para armazenamento de símbolos de debug.
    Symbol, símbolo Um nome de variável ou função, normalmente dado pelo programador ao construir o código fonte. Debuggers modernos associam automaticamente endereços de memória a símbolos, facilitando o debug.
    Section, seção Um formato de representação de arquivo binário como ELF ou COFF divide o arquivo em seções, que contém tipos diversos de informação. Uma ou mais seções podem estar dedicadas a armazenar informações de debug, como símbolos.
    Session, sessão uma sessão de debug, uma rodada de execução de um programa que está sendo debugado.

    Vamos por logo a mão na massa: Usando o projeto esqueleto, fiz um programa para ser debugado, está aqui. Agora só precisamos iniciar uma sessão de debug usando GDB.

    Aqui está o código fonte, para facilitar o acompanhamento:

    /*
     *  Copyright (C) 2009 William Cesar de Oliveira
     *
     *  This program is free software: you can redistribute it and/or modify
     *  it under the terms of the GNU General Public License as published by
     *  the Free Software Foundation, either version 2 of the License, or
     *  (at your option) any later version.
     *
     *  This program is distributed in the hope that it will be useful,
     *  but WITHOUT ANY WARRANTY; without even the implied warranty of
     *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     *  GNU General Public License for more details.
     *
     *  You should have received a copy of the GNU General Public License
     *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
     */
    
    #include <stdio.h>
    #include <stdlib.h>
    
    #define TEST_VALUE 100
    
    int v_global = TEST_VALUE;
    
    int main(int argc, char *argv[])
    {
    	int i;
    	FILE *my_out = NULL;
    
    	my_out = stdout;
    	for (i = 0; i < TEST_VALUE; i++, v_global--)
    		fprintf(my_out, "i=%d global=%d\n", i, v_global);
    	return 0;
    }
    

    Para começar, compile o programa usando

    # make clean release

    E, depois execute o debugger com

    # gdb ./gdb0

    Com essa linha de comando o GDB vai carregar, mostrar seu o prompt e ficar aguardando um comando. Para fazer o programa executar,

    gdb> run

    E pronto, o programa debugado está executando sob a tutela do GDB. Fácil, não? Tanto a entrada como a saída padrão do programa (para onde vão os printf) são as mesmas do GDB, portanto, ao término do programa, o controle retorna para o GDB, mas o console continua o mesmo.

    Quando qualquer condição anormal acontece no programa (um erro, fatal signal, exceção ou um breakpoint), o prompt do GDB retorna, e podemos examinar ou modificar o estado do programa debugado. Quando o programa debugado termina o controle também retorna para o GDB.

    Logo quando o GDB inicia, é mostrada uma mensagem sobre o carregamento dos símbolos, no caso desse primeiro teste, a mensagem é:

    Reading symbols from ./gdb0...(no debugging symbols found)...done.

    Isso significa que o GDB não foi capaz de encontrar as informações necessárias para associar nomes de variáveis, funções e linhas com o programa em execução. Na prática, quer dizer que tudo o que você vai ver são instruções assembler, conteúdo de registradores e endereços de memória, sem nenhuma associação com os nomes usados no código fonte do programa.

    O GDB ainda é útil para debugar programas que não contém os símbolos de debug, mas o processo fica muito mais difícil, exigindo conhecimentos da arquitetura do processador, um pouco do funcionamento interno do sistema operacional e muita paciência.

    Podemos examinar o conteúdo de um binário qualquer usando o utilitário objdump. No caso desse programa de testes gdb0:

    # objdump -h gdb0
    gdb0: file format elf64-x86-64
    Sections:
    Idx Name Size VMA LMA File off Algn
    0 .interp 0000001c 0000000000400200 0000000000400200 00000200 2**0
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    1 .note.ABI-tag 00000020 000000000040021c 000000000040021c 0000021c 2**2
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    2 .note.gnu.build-id 00000024 000000000040023c 000000000040023c 0000023c 2**2
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    3 .gnu.hash 0000001c 0000000000400260 0000000000400260 00000260 2**3
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    4 .dynsym 00000060 0000000000400280 0000000000400280 00000280 2**3
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    5 .dynstr 0000003f 00000000004002e0 00000000004002e0 000002e0 2**0
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    6 .gnu.version 00000008 0000000000400320 0000000000400320 00000320 2**1
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    7 .gnu.version_r 00000020 0000000000400328 0000000000400328 00000328 2**3
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    8 .rela.dyn 00000018 0000000000400348 0000000000400348 00000348 2**3
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    9 .rela.plt 00000030 0000000000400360 0000000000400360 00000360 2**3
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    10 .init 00000018 0000000000400390 0000000000400390 00000390 2**2
    CONTENTS, ALLOC, LOAD, READONLY, CODE
    11 .plt 00000030 00000000004003a8 00000000004003a8 000003a8 2**2
    CONTENTS, ALLOC, LOAD, READONLY, CODE
    12 .text 000001f8 00000000004003e0 00000000004003e0 000003e0 2**4
    CONTENTS, ALLOC, LOAD, READONLY, CODE
    13 .fini 0000000e 00000000004005d8 00000000004005d8 000005d8 2**2
    CONTENTS, ALLOC, LOAD, READONLY, CODE
    14 .rodata 00000020 00000000004005e8 00000000004005e8 000005e8 2**3
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    15 .eh_frame_hdr 00000024 0000000000400608 0000000000400608 00000608 2**2
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    16 .eh_frame 0000007c 0000000000400630 0000000000400630 00000630 2**3
    CONTENTS, ALLOC, LOAD, READONLY, DATA
    17 .ctors 00000010 00000000006006b0 00000000006006b0 000006b0 2**3
    CONTENTS, ALLOC, LOAD, DATA
    18 .dtors 00000010 00000000006006c0 00000000006006c0 000006c0 2**3
    CONTENTS, ALLOC, LOAD, DATA
    19 .jcr 00000008 00000000006006d0 00000000006006d0 000006d0 2**3
    CONTENTS, ALLOC, LOAD, DATA
    20 .dynamic 00000190 00000000006006d8 00000000006006d8 000006d8 2**3
    CONTENTS, ALLOC, LOAD, DATA
    21 .got 00000008 0000000000600868 0000000000600868 00000868 2**3
    CONTENTS, ALLOC, LOAD, DATA
    22 .got.plt 00000028 0000000000600870 0000000000600870 00000870 2**3
    CONTENTS, ALLOC, LOAD, DATA
    23 .data 00000004 0000000000600898 0000000000600898 00000898 2**2
    CONTENTS, ALLOC, LOAD, DATA
    24 .bss 00000010 00000000006008a0 00000000006008a0 0000089c 2**3
    ALLOC
    25 .comment 0000013f 0000000000000000 0000000000000000 0000089c 2**0
    CONTENTS, READONLY

    Nota-se que é um binário no formato ELF, 64 bits, para arquitetura x86_64. Vamos recompilar e debugar o mesmo programa novamente, agora usando

    # make clean debug

    Executando o objdump novamente, podemos ver que existem algumas seções que não estavam presente na versão release:

    ...
    26 .debug_aranges 00000030 0000000000000000 0000000000000000 000009eb 2**0
    CONTENTS, READONLY, DEBUGGING
    27 .debug_pubnames 0000001b 0000000000000000 0000000000000000 00000a1b 2**0
    CONTENTS, READONLY, DEBUGGING
    28 .debug_info 000000da 0000000000000000 0000000000000000 00000a36 2**0
    CONTENTS, READONLY, DEBUGGING
    29 .debug_abbrev 00000075 0000000000000000 0000000000000000 00000b10 2**0
    CONTENTS, READONLY, DEBUGGING
    30 .debug_line 00000214 0000000000000000 0000000000000000 00000b85 2**0
    CONTENTS, READONLY, DEBUGGING
    31 .debug_str 000000ba 0000000000000000 0000000000000000 00000d99 2**0
    CONTENTS, READONLY, DEBUGGING
    32 .debug_macinfo 000048d6 0000000000000000 0000000000000000 00000e53 2**0
    CONTENTS, READONLY, DEBUGGING
    33 .debug_pubtypes 00000012 0000000000000000 0000000000000000 00005729 2**0
    CONTENTS, READONLY, DEBUGGING

    Como mencionado, essas seções servirão para ajudar um debugger como o GDB a relacionar os símbolos com os endereços de memória e nomes de função. Embora as informações de debug não precisem estar necessariamente embutidas no próprio binário, vamos usar esse modelo por ser mais simples.

    Agora, veremos a seguinte mensagem quando o GDB carregar o programa:

    # gdb ./gdb0
    Reading symbols from ./gdb0...done.

    Essa singela mensagem significa que o GDB vai ser capaz de correlacionar os símbolos do código fonte com o programa em execução.

    Para construir um programa com os símbolos de debug, temos que instruir o compilador/linker para fazer isso. No caso do GCC, o comando -g está associado a debugging. Se usarmos o comando -ggdb, estaremos instruindo o GCC a adicionar informações de debug no formato ideal para o GDB. Outra opção que afeta o debugger é a otimização, o programa ideal para debug é o que foi compilado com todas as opções de otimização desligadas (no caso do GCC, -O0). Isso porque, durante a otimização, o compilador pode mover/remover varíaveis, funções e trechos de código, tornando mais difícil (mas não impossível) o processo de debug. Para mais informações, consulte o manual do GCC.

    Um programa normalmente faz uso de bibliotecas carregadas dinamicamente, as famosas libs, sendo que a libc é usada em 99.9% dos programas. O GDB é capaz de carregar os símbolos tanto para o executável propriamente dito como para as libs, bastando que os símbolos estejam disponíveis para serem automaticamente carregados.

    Voltando ao debug, usando start ao invés de run faremos com que o progama pare na primeira linha com side-effect do código fonte:

    (gdb) start
    Temporary breakpoint 1 at 0x400514: file main.c, line 28.
    Starting program: ./gdb0
    Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe1a8) at main.c:28
    28 FILE *my_out = NULL;

    O GDB colocou um breakpoint temporário na linha 28 antes de executar o programa. Um breakpoint é usado para indicar ao GDB que a execução do programa debugado deve ser suspensa e o controle deve ser passado ao GDB.

    Como o programa está suspenso, podemos examinar a pilha de chamadas para a função atual (main):

    (gdb) bt
    #0 main (argc=1, argv=0x7fffffffe1a8) at main.c:28

    Também podemos examinar a pilha incluindo as variáveis locais:

    #0 main (argc=1, argv=0x7fffffffe1a8) at main.c:28
    i = 0
    my_out = 0x400420

    Vemos que estamos na função main, que foi chamada com argc=1 e argv=0x7fffffffe1a8. Todo mundo sabe que argv é um array de ponteiros, mas será que o GDB sabe?

    (gdb) whatis argv
    type = char **
    (gdb) whatis argc
    type = int
    (gdb) whatis my_out
    type = FILE *
    (gdb) whatis i
    type = int
    (gdb) whatis v_global
    type = int
    (gdb) whatis TEST_VALUE
    type = int
    (gdb) p TEST_VALUE
    $1 = 100

    Como podemos ver, o GDB também identifica e mostra macros, se o formato de armazenamento dos símbolos permitir.

    Se quisermos informação detalhada sobre o tipo de uma variável, podemos usar o ptype:

    (gdb) ptype my_out
    type = struct _IO_FILE {
    int _flags;
    char *_IO_read_ptr;
    char *_IO_read_end;
    char *_IO_read_base;
    char *_IO_write_base;
    char *_IO_write_ptr;
    char *_IO_write_end;
    char *_IO_buf_base;
    char *_IO_buf_end;
    char *_IO_save_base;
    char *_IO_backup_base;
    char *_IO_save_end;
    struct _IO_marker *_markers;
    struct _IO_FILE *_chain;
    int _fileno;
    int _flags2;
    __off_t _old_offset;
    short unsigned int _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];
    _IO_lock_t *_lock;
    __off64_t _offset;
    void *__pad1;
    void *__pad2;
    void *__pad3;
    void *__pad4;
    size_t __pad5;
    int _mode;
    char _unused2[20];
    } *

    Note que apesar de FILE não ter sido definida em nosso código fonte, podemos ver a definição da struct.

    Se quisermos examinar o conteúdo ou o endereço de memória de uma variável, por exemplo, v_global:

    (gdb) p v_global
    $1 = 100

    O valor mostrado, 100, é o conteúdo de v_global. Para saber o endereço de v_global,

    (gdb) p &v_global
    $2 = (int *) 0x600924

    Será que a inversa é possível, a partir de um endereço saber o nome da variável?
    Para variáveis globais, a resposta é sim.

    (gdb) info symbol 0x600924
    v_global in section .data of gdb0

    Para variáveis locais, no entanto...

    (gdb) p &i
    $3 = (int *) 0x7fffffffe0a4
    (gdb) info symbol 0x7fffffffe0a4
    No symbol matches 0x7fffffffe0a4

    Agora que já examinamos algumas variáveis, agora vamos fazer o programa continuar para a próxima linha do código fonte. Uma linha de código fonte geralmente corresponde a várias instruções de máquina, mas não precisamos nos preocupar com isso, o GDB usa as anotações colocadas pelo compilador para calcular automaticamente onde o breakpoint será colocado.

    (gdb) step
    30 my_out = stdout;

    Esse comando instrue o GDB para executar uma ou mais linhas de código fonte, sendo o default a execução de 1 linha. A linha mostrada após o comando é a próxima a ser executada.
    Quando encontra uma chamada de função, o step segue para dentro daquela função. Existe um outro comando, o next, que funciona como o step, mas não segue uma função. Ao encontrar uma chamada de função, o next chama a função e para a execução após o retorno. Tanto step quanto next implementam a variação i, stepi e nexti, que funcionam como o comando original, mas executando uma instrução de máquina ao invés de uma linha de código.

    No nosso programa de teste, verificando os valores das variáveis, obteremos:

    (gdb) info locals
    i = 0
    my_out = 0x0

    Só para lembrar, ao comandarmos step, a linha executada foi

    28 FILE *my_out = NULL;

    Por isso observamos que o valor de my_out agora é NULL (0x0).

    Quando estamos executando um programa complexo, com muitos arquivos fonte e vários breakpoints, podemos ter problemas para lembrar qual o arquivo fonte que está relacionado ao contexto atual. Felizmente podemos resolver esse problema com:

    (gdb) info source
    Current source file is main.c
    Compilation directory is gdb0
    Located in ./gdb0/main.c
    Contains 34 lines.
    Source language is c.
    Compiled with DWARF 2 debugging format.
    Does not include preprocessor macro info.

    Agora que já lembramos o que estávamos fazendo, vamos mandar o programa parar logo após o loop, na linha 33. Sabendo em qual linha queremos definir um breakpoint, temos 2 formas de informar isso ao GDB. Se a linha estiver no mesmo arquivo, como é o caso aqui, podemos simplesmente:

    (gdb) break 33
    Breakpoint 2 at 0x40056b: file main.c, line 33.

    Isso significa que o GDB vai colocar um breakpoint na instrução que está no endereço 0x40056b, que corresponde à linha 33 do arquivo main.c.
    Quando o breakpoint a ser definido não está no arquivo atual, ou quando o programa ainda não está em execução (e possui vários fontes), temos que informar o arquivo fonte, além do número da linha:

    (gdb) break main.c:33
    Breakpoint 1 at 0x40056b: file main.c, line 33.

    Vamos então mandar o GDB continuar com a execução do programa:

    (gdb) c
    Continuing.
    i=0 global=100
    i=1 global=99
    i=2 global=98
    i=3 global=97
    ...
    i=98 global=2
    i=99 global=1
    Breakpoint 1, main (argc=1, argv=0x7fffffffe1a8) at main.c:33
    33 return 0;

    O GDB suspendeu a execução do programa ANTES de executar a linha marcada. Vamos examinar o valor das variáveis:

    (gdb) info locals
    i = 100
    my_out = 0x3cc7173780
    (gdb) p v_global
    $1 = 0

    É, parece que funciona. Antes de continuar a execução, no entanto, vamos mudar o fluxo de execução, fazendo o programa redefinir qual a próxima linha/instrução a ser executada:

    (gdb) jump 31
    Continuing at 0x400527.
    i=0 global=0
    i=1 global=-1
    i=2 global=-2
    ...
    i=99 global=-99
    Breakpoint 2, main (argc=1, argv=0x7fffffffe1a8) at main.c:33
    33 return 0;

    Veja que a variável v_global não foi reinicializada, por isso ficou com os valores negativos.
    Por último, vamos deixar o programa terminar em paz:

    (gdb) c
    Continuing.
    Program exited normally.

    Uma extensão interessante de breakpoint é o breakpoint condicional, que só pára o programa quando uma determinada condição for satisfeita. Por exemplo, vamos adicionar um breakpoint condicional na linha 32, para que o breakpoint funcione somente quando o valor de i for 50 e, em seguida, mandaremos o programa executar:

    (gdb) break main.c:32 if i == 50
    Breakpoint 3 at 0x400530: file main.c, line 32.
    (gdb) run
    Starting program: ./gdb0
    i=0 global=100
    ...
    i=49 global=51
    Breakpoint 3, main (argc=1, argv=0x7fffffffe1a8) at main.c:32
    32 fprintf(my_out, "i=%d global=%d\n", i, v_global);

    Examinando os valores das variáveis:

    (gdb) info locals
    i = 50
    my_out = 0x3cc7173780

    Vamos aproveitar o breakpoint para alterar o valor da variável i para 200:

    (gdb) set variable i=200
    (gdb) p i
    $2 = 200

    E agora mandamos o programa continuar:

    (gdb) c
    Continuing.
    i=200 global=50

    Se você não reiniciou o GDB, aquele breakpoint que colocamos na linha 33 continua lá...

    Breakpoint 1, main (argc=1, argv=0x7fffffffe1a8) at main.c:33
    33 return 0;

    Examinando as variáveis novamente, teremos:

    Breakpoint 1, main (argc=1, argv=0x7fffffffe1a8) at main.c:33
    33 return 0;
    (gdb) p i
    $3 = 201
    (gdb) p v_global
    $4 = 49

    E vamos terminar a execução normalmente:

    (gdb) c
    Continuing.
    Program exited normally.

    Mudamos o estado interno do programa durante a parada. O mecanismo de breakpoint permite fazer isso automaticamente, através da definição de uma lista de comandos a serem executados quando o breakpoint é ativado.

    Vamos começar removendo o breakpoint da linha 32:

    (gdb) info break
    Num Type Disp Enb Address What
    2 breakpoint keep y 0x000000000040056b in main at main.c:33
    3 breakpoint keep y 0x0000000000400530 in main at main.c:32
    stop only if i == 50

    Queremos remover o breakpoint #3:

    (gdb) delete 3

    Agora vamos adicioná-lo novamente, definindo a condição para quando o valor de v_global for 47:

    (gdb) break 32 if v_global == 47
    Breakpoint 4 at 0x400530: file main.c, line 32.

    Note que o slot 3 não é reutilizado. Poderíamos, ao invés de remover e adicionar, somente alterar as condições do breakpoint. Por exemplo, para alterar a condição do breakpoint 4 para parar quando v_global for 49:

    (gdb) condition 4 v_global == 49
    (gdb) info break
    Num Type Disp Enb Address What
    2 breakpoint keep y 0x000000000040056b in main at main.c:33
    breakpoint already hit 1 time
    4 breakpoint keep y 0x0000000000400530 in main at main.c:32
    stop only if v_global == 49

    Agora vamos fazer com que, quando o breakpoint 4 for ativado, o valor da variável i seja alterado para 200.

    (gdb) commands 4
    Type commands for when breakpoint 4 is hit, one per line.
    End with a line saying just "end".
    >set var i = 200
    >end

    E agora, vamos executar o programa para ver o resultado:

    (gdb) run
    Starting program: ./gdb0
    i=0 global=100
    ...
    i=50 global=50
    Breakpoint 4, main (argc=1, argv=0x7fffffffe1a8) at main.c:32
    32 fprintf(my_out, "i=%d global=%d\n", i, v_global);

    Olhando para os valores das variáveis:

    (gdb) p i
    $1 = 200
    (gdb) p v_global
    $2 = 49

    Podemos ver que o breakpoint executou os comandos que estavam previstos. Continuando com o programa:

    (gdb) c
    Continuing.
    i=200 global=49
    Breakpoint 2, main (argc=1, argv=0x7fffffffe1a8) at main.c:33
    33 return 0;

    Aquele breakpoint na linha 33 continua lá. Finalmente:

    (gdb) c
    Continuing.
    Program exited normally.

    Além de redefinir os valores de variáveis, também podemos chamar funções a partir lista de comandos associados ao breakpoint. Por exemplo, vamos alterar a lista de comandos do breakpoint da linha 32 e reiniciar o programa:

    (gdb) info break
    Num Type Disp Enb Address What
    2 breakpoint keep y 0x000000000040056b in main at main.c:33
    breakpoint already hit 1 time
    4 breakpoint keep y 0x0000000000400530 in main at main.c:32
    stop only if v_global == 49
    breakpoint already hit 1 time
    set var i = 200
    (gdb) commands 4
    Type commands for when breakpoint 4 is hit, one per line.
    End with a line saying just "end".
    >call puts("Hello Breakpoint!\n")
    >end
    (gdb) run
    Starting program: ./gdb0
    i=0 global=100
    i=1 global=99
    ...
    i=50 global=50
    Breakpoint 4, main (argc=1, argv=0x7fffffffe1a8) at main.c:32
    32 fprintf(my_out, "i=%d global=%d\n", i, v_global);
    Hello Breakpoint!
    $3 = 19

    Lembre-se de que a função é chamada dentro do contexto do debuggee, usando o stack frame corrente. O valor 19 é o retorno da função puts. Quaisquer funções podem ser chamadas, estejam em bibliotecas ou no programa debugado.

    É importante notar que a lista de comandos substituiu completamente a anterior, por isso, ao examinarmos o valor de i:

    (gdb) p i
    $4 = 51

    Frequentemente queremos que o breakpoint seja ativado somente após algumas vezes depois que o programa possou por ele. O GDB já mantém um contador de vezes que um breakpoint foi ativado, e o comando ignore serve para isso. Vamos limpar a lista de breakpoints, adicionar um breakpoint na linha 32 e fazer com que o programa seja suspenso após passar pelo breakpoint por 7 vezes:

    (gdb) delete breakpoints
    Delete all breakpoints? (y or n) y
    (gdb) break 32
    Breakpoint 4 at 0x400530: file main.c, line 32.
    (gdb) info break
    Num Type Disp Enb Address What
    4 breakpoint keep y 0x0000000000400530 in main at main.c:32
    (gdb) ignore 4 7
    Will ignore next 7 crossings of breakpoint 4.

    Executando o programa novamente:

    (gdb) run
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Starting program: ./gdb0
    i=0 global=100
    i=1 global=99
    i=2 global=98
    i=3 global=97
    i=4 global=96
    i=5 global=95
    i=6 global=94
    Breakpoint 4, main (argc=1, argv=0x7fffffffe1a8) at main.c:32
    32 fprintf(my_out, "i=%d global=%d\n", i, v_global);

    Veja também enable, disable, finish, until e advance.

    Chega de breakpoints, vamos limpar todos os breakpoints e reiniciar o programa. O assunto agora é watchpoints.


    (gdb) delete breakpoints
    Delete all breakpoints? (y or n) y
    (gdb) start
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Temporary breakpoint 5 at 0x400514: file main.c, line 28.
    Starting program: ./gdb0
    Temporary breakpoint 5, main (argc=1, argv=0x7fffffffe1a8) at main.c:28
    28 FILE *my_out = NULL;

    Um watchpoint nada mais é do que um "breakpoint de dados". O tipo de breakpoint que vimos até agora está relacionado à um endereço de memória de programa, e é ativado quando o registrador de instrução (IP na arquitetura x86) executa o endereço marcado (na verdade é mais tosco do que essa descrição poética, mas deixo essa descoberta para você). Um watchpoint funciona de modo parecido, mas é ativado pela avaliação de uma expressão, sem importar ONDE os termos da expressão foram alterados. Por exemplo:

    (gdb) watch v_global == 90
    Hardware watchpoint 2: v_global == 90
    (gdb) c
    Continuing.
    i=0 global=100
    i=1 global=99
    i=2 global=98
    i=3 global=97
    i=4 global=96
    i=5 global=95
    i=6 global=94
    i=7 global=93
    i=8 global=92
    i=9 global=91
    Hardware watchpoint 2: v_global == 90
    Old value = 0
    New value = 1
    0x0000000000400565 in main (argc=1, argv=0x7fffffffe188) at main.c:31
    31 for (i = 0; i < TEST_VALUE; i++, v_global--)

    Bom, você deve estar pensando: "eu poderia fazer o mesmo com um breakpoint condicional!". Como esse caso é muito simples, realmente isso poderia ser feito. Entretanto, imagine uma variável que pode ter o valor alterado em vários lugares diferentes, ou se você desejar que o watchpoint pare o programa somente quando uma certa variável for lida: nesse caso, watchpoint é a melhor (única?) opção.

    Fazendo o programa continuar:

    (gdb) c
    Continuing.
    i=10 global=90
    Hardware watchpoint 2: v_global == 90
    Old value = 1
    New value = 0
    0x0000000000400565 in main (argc=1, argv=0x7fffffffe188) at main.c:31
    31 for (i = 0; i < TEST_VALUE; i++, v_global--)

    E ele parou de novo, por causa da transição (houve uma referência à expressão).
    Vamos apagar todos os breapoints novamente, adicionar um rwatch no lugar e executar o programa:

    (gdb) delete breakpoints
    Delete all breakpoints? (y or n) y
    (gdb) rwatch v_global
    Hardware read watchpoint 3: v_global
    (gdb) run
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Starting program: ./gdb0
    Hardware read watchpoint 3: v_global
    Value = 100
    0x0000000000400536 in main (argc=1, argv=0x7fffffffe188) at main.c:32
    32 fprintf(my_out, "i=%d global=%d\n", i, v_global);

    Podemos ver que o programa parou na primeira vez que o valor de v_global foi lido (e não é no for :) ).
    Se quiser, continue com c até cansar. Note que o watchpoint é tratado como breakpoint quando se trata de remoção.

    Um detalhe importante é a diferença entre Hardware Watchpoint e Software Watchpoint. Quando usamos Hardware Watchpoint, o GDB é capaz de detectar e parar o programa na próxima instrução, funcionando mesmo que a referência/alteração seja feita em outras threads. Software Watchpoints, por outro lado, tem granularidade de statement. Entretanto, nada disso é grátis: Hardware watchpoints tem número limitado. Veja também tracepoint.

    Depois dessa overdose de breakpoints, vamos descansar um pouco e ver como verificar o conteúdo dos registradores da CPU. Mas antes vamos aproveitar e deixar tudo limpinho:

    (gdb) delete breakpoints
    Delete all breakpoints? (y or n) y
    (gdb) start
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Temporary breakpoint 5 at 0x400514: file main.c, line 28.
    Starting program: ./gdb0
    Temporary breakpoint 5, main (argc=1, argv=0x7fffffffe1a8) at main.c:28
    28 FILE *my_out = NULL;
    (gdb) info register
    rax 0x3cc7175f60 261038235488
    rbx 0x0 0
    rcx 0x0 0
    rdx 0x7fffffffe1b8 140737488347576
    rsi 0x7fffffffe1a8 140737488347560
    rdi 0x1 1
    rbp 0x7fffffffe0c0 0x7fffffffe0c0
    rsp 0x7fffffffe090 0x7fffffffe090
    r8 0x3cc7174300 261038228224
    r9 0x3cc6a0e4d0 261030470864
    r10 0x7fffffffdf20 140737488346912
    r11 0x3cc6e1ea20 261034732064
    r12 0x400420 4195360
    r13 0x7fffffffe1a0 140737488347552
    r14 0x0 0
    r15 0x0 0
    rip 0x400514 0x400514
    eflags 0x206 [ PF IF ]
    cs 0x33 51
    ss 0x2b 43
    ds 0x0 0
    es 0x0 0
    fs 0x0 0
    gs 0x0 0
    fctrl 0x37f 895
    fstat 0x0 0
    ftag 0xffff 65535
    fiseg 0x0 0
    fioff 0x0 0
    foseg 0x0 0
    fooff 0x0 0
    fop 0x0 0
    mxcsr 0x1f80 [ IM DM ZM OM UM PM ]

    Para ver o conteúdo de todos os registradores, especialmente os de multimedia (se houver), comandamos

    (gdb) info all-registers
    rax 0x3cc7175f60 261038235488
    rbx 0x0 0
    rcx 0x0 0
    rdx 0x7fffffffe1b8 140737488347576
    rsi 0x7fffffffe1a8 140737488347560
    rdi 0x1 1
    rbp 0x7fffffffe0c0 0x7fffffffe0c0
    rsp 0x7fffffffe090 0x7fffffffe090
    r8 0x3cc7174300 261038228224
    r9 0x3cc6a0e4d0 261030470864
    r10 0x7fffffffdf20 140737488346912
    r11 0x3cc6e1ea20 261034732064
    r12 0x400420 4195360
    r13 0x7fffffffe1a0 140737488347552
    r14 0x0 0
    r15 0x0 0
    rip 0x400514 0x400514
    eflags 0x206 [ PF IF ]
    cs 0x33 51
    ss 0x2b 43
    ds 0x0 0
    es 0x0 0
    fs 0x0 0
    gs 0x0 0
    st0 0 (raw 0x00000000000000000000)
    st1 0 (raw 0x00000000000000000000)
    st2 0 (raw 0x00000000000000000000)
    st3 0 (raw 0x00000000000000000000)
    st4 0 (raw 0x00000000000000000000)
    st5 0 (raw 0x00000000000000000000)
    st6 0 (raw 0x00000000000000000000)
    st7 0 (raw 0x00000000000000000000)
    fctrl 0x37f 895
    fstat 0x0 0
    ftag 0xffff 65535
    fiseg 0x0 0
    fioff 0x0 0
    foseg 0x0 0
    fooff 0x0 0
    fop 0x0 0
    xmm0 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0xff, 0x0 }, v8_int16 = {0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0xff, 0x0, 0x0, 0x0}, v2_int64 = {0xff, 0x0}, uint128 = 0x000000000000000000000000000000ff}
    xmm1 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x2f }, v8_int16 = {0x2f2f, 0x2f2f, 0x2f2f, 0x2f2f, 0x2f2f, 0x2f2f, 0x2f2f, 0x2f2f}, v4_int32 = {0x2f2f2f2f, 0x2f2f2f2f, 0x2f2f2f2f, 0x2f2f2f2f}, v2_int64 = {
    0x2f2f2f2f2f2f2f2f, 0x2f2f2f2f2f2f2f2f}, uint128 = 0x2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f}
    xmm2 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm3 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v8_int16 = {0x0, 0x0, 0xff00, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0xff00, 0x0, 0x0}, v2_int64 = {
    0xff0000000000, 0x0}, uint128 = 0x00000000000000000000ff0000000000}
    xmm4 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm5 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm6 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm7 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm8 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm9 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm10 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm11 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm12 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm13 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm14 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    xmm15 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 }, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
    mxcsr 0x1f80 [ IM DM ZM OM UM PM ]

    Para examinar a FPU:

    (gdb) info float
    R7: Empty 0x00000000000000000000
    R6: Empty 0x00000000000000000000
    R5: Empty 0x00000000000000000000
    R4: Empty 0x00000000000000000000
    R3: Empty 0x00000000000000000000
    R2: Empty 0x00000000000000000000
    R1: Empty 0x00000000000000000000
    =>R0: Empty 0x00000000000000000000
    Status Word: 0x0000
    TOP: 0
    Control Word: 0x037f IM DM ZM OM UM PM
    PC: Extended Precision (64-bits)
    RC: Round to nearest
    Tag Word: 0xffff
    Instruction Pointer: 0x00:0x00000000
    Operand Pointer: 0x00:0x00000000
    Opcode: 0x0000

    Alternativamente, se quisermos examinar o valor de um registrador específico:

    (gdb) p/x $rax
    $7 = 0x3cc7175f60

    O /x nesse caso serve para que o resultado seja mostrado em formato hexadecimal. A especificação de formato pode ser usada em várias situações.

    Para alterar o valor de RAX para NULL:

    (gdb) set var $rax = 0

    Agora, se pedirmos para ver novamente o valor de rax:

    (gdb) p/x $rax
    $8 = 0x0

    Podemos alterar o valor de qualquer registrador. Claro que, dependendo do valor e do registrador, também poderemos provocar resultados catastróficos.

    Às vezes pode ser útil ver as instruções de máquina decodificadas de uma região do programa. Quando não temos informação de debug disponível, essa pode ser a única forma. Por exemplo, para ver as instruções que compoe a função main:

    (gdb) disas main
    Dump of assembler code for function main:
    0x0000000000400504 : push %rbp
    0x0000000000400505 : mov %rsp,%rbp
    0x0000000000400508 : push %rbx
    0x0000000000400509 : sub $0x28,%rsp
    0x000000000040050d : mov %edi,-0x24(%rbp)
    0x0000000000400510 : mov %rsi,-0x30(%rbp)
    0x0000000000400514 : movq $0x0,-0x18(%rbp)
    0x000000000040051c : mov 0x20040d(%rip),%rax # 0x600930
    0x0000000000400523 : mov %rax,-0x18(%rbp)
    0x0000000000400527 : movl $0x0,-0x1c(%rbp)
    0x000000000040052e : jmp 0x400565
    0x0000000000400530 : mov 0x2003ee(%rip),%ecx # 0x600924
    0x0000000000400536 : mov $0x400678,%ebx
    0x000000000040053b : mov -0x1c(%rbp),%edx
    0x000000000040053e : mov -0x18(%rbp),%rax
    0x0000000000400542 : mov %rbx,%rsi
    0x0000000000400545 : mov %rax,%rdi
    0x0000000000400548 : mov $0x0,%eax
    0x000000000040054d : callq 0x400410
    0x0000000000400552 : addl $0x1,-0x1c(%rbp)
    0x0000000000400556 : mov 0x2003c8(%rip),%eax # 0x600924
    0x000000000040055c : sub $0x1,%eax
    0x000000000040055f : mov %eax,0x2003bf(%rip) # 0x600924
    0x0000000000400565 : cmpl $0x63,-0x1c(%rbp)
    0x0000000000400569 : jle 0x400530
    0x000000000040056b : mov $0x0,%eax
    0x0000000000400570 : add $0x28,%rsp
    0x0000000000400574 : pop %rbx
    0x0000000000400575 : leaveq
    0x0000000000400576 : retq
    End of assembler dump.

    Para descobrir onde está uma linha específica, por exemplo, a linha 32 do main.c:

    (gdb) info line 32
    Line 32 of "main.c" starts at address 0x400530 and ends at 0x400552 .

    Por fim, se quisermos que a função main seja decodificada juntamente com uma anotação mostrando onde cada linha começa:

    (gdb) disas /m main
    Dump of assembler code for function main:
    26 {
    0x0000000000400504 : push %rbp
    0x0000000000400505 : mov %rsp,%rbp
    0x0000000000400508 : push %rbx
    0x0000000000400509 : sub $0x28,%rsp
    0x000000000040050d : mov %edi,-0x24(%rbp)
    0x0000000000400510 : mov %rsi,-0x30(%rbp)
    27 int i;
    28 FILE *my_out = NULL;
    0x0000000000400514 : movq $0x0,-0x18(%rbp)
    29
    30 my_out = stdout;
    0x000000000040051c : mov 0x20040d(%rip),%rax # 0x600930
    0x0000000000400523 : mov %rax,-0x18(%rbp)
    31 for (i = 0; i < TEST_VALUE; i++, v_global--)
    0x0000000000400527 : movl $0x0,-0x1c(%rbp)
    0x000000000040052e : jmp 0x400565
    0x0000000000400552 : addl $0x1,-0x1c(%rbp)
    0x0000000000400556 : mov 0x2003c8(%rip),%eax # 0x600924
    0x000000000040055c : sub $0x1,%eax
    0x000000000040055f : mov %eax,0x2003bf(%rip) # 0x600924
    0x0000000000400565 : cmpl $0x63,-0x1c(%rbp)
    0x0000000000400569 : jle 0x400530
    32 fprintf(my_out, "i=%d global=%d\n", i, v_global);
    0x0000000000400530 : mov 0x2003ee(%rip),%ecx # 0x600924
    0x0000000000400536 : mov $0x400678,%ebx
    0x000000000040053b : mov -0x1c(%rbp),%edx
    0x000000000040053e : mov -0x18(%rbp),%rax
    0x0000000000400542 : mov %rbx,%rsi
    0x0000000000400545 : mov %rax,%rdi
    0x0000000000400548 : mov $0x0,%eax
    0x000000000040054d : callq 0x400410
    33 return 0;
    0x000000000040056b : mov $0x0,%eax
    34 }
    0x0000000000400570 : add $0x28,%rsp
    0x0000000000400574 : pop %rbx
    0x0000000000400575 : leaveq
    0x0000000000400576 : retq
    End of assembler dump.

    Agora que já sabemos como colocar breakpoints, examinar e alterar o valor de variáveis, vamos ver como fazemos para bisbilhotar uma região de memória. Por exemplo, para examinar o valor da região de memória próxima à variável stdout, podemos usar o comando x:

    (gdb) x/16xb 0x3cc7173d70
    0x3cc7173d70 : 0x80 0x37 0x17 0xc7 0x3c 0x00 0x00 0x00
    0x3cc7173d78 : 0x60 0x38 0x17 0xc7 0x3c 0x00 0x00 0x00
    Veja que o GDB adiciona o nome do símbolo mais próximo da área examinada, se houver. Sabemos que stdout é um ponteiro, então os 8 primeiros bytes correspondem ao endereço da área onde está a struct (em little endian, invertido):
    0x80 0x37 0x17 0xc7 0x3c 0x00 0x00 0x00
    Colocando na ordem correta, temos:
    0000003cc7173780.

    Comparando com o valor de stdout:

    (gdb) p stdout
    $13 = (struct _IO_FILE *) 0x3cc7173780

    O comando x aceita alguns parâmetros, que especificam a quantidade (16), o tipo de formatação (x=hexadecimal) e o tamanho dos dados (b=byte) que queremos ver no dump.

    Os exemplos acima usaram o GDB para carregar e executar o programa, mas não tem de ser assim. Podemos fazer o GDB debugar um programa que já está em execução. Por exemplo, vamos fazer uma pequena alteração no programa gdb0, e chamá-lo de gdb1, para que fique assim:

    /*
     *  Copyright (C) 2009 William Cesar de Oliveira
     *
     *  This program is free software: you can redistribute it and/or modify
     *  it under the terms of the GNU General Public License as published by
     *  the Free Software Foundation, either version 2 of the License, or
     *  (at your option) any later version.
     *
     *  This program is distributed in the hope that it will be useful,
     *  but WITHOUT ANY WARRANTY; without even the implied warranty of
     *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     *  GNU General Public License for more details.
     *
     *  You should have received a copy of the GNU General Public License
     *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
     */
    
    #include <stdio.h>
    #include <stdlib.h>
    
    #define TEST_VALUE 100
    
    int v_global = TEST_VALUE;
    
    int main(int argc, char *argv[])
    {
    	int i;
    	FILE *my_out = NULL;
    
    	sleep(60);
    	my_out = stdout;
    	for (i = 0; i < TEST_VALUE; i++, v_global--)
    		fprintf(my_out, "i=%d global=%d\n", i, v_global);
    	return 0;
    }
    

    Com essa alteração, o programa vai esperar 60 segundos antes de continuar. Assim, teremos tempo de executar o GDB (espero). Agora, executamos o programa em segundo plano:

    # ./gdb1 &

    O shell vai colocar o número do PID do programa. Se não o fizer, podemos descobrir com:

    # ps -C gdb1
    PID TTY TIME CMD
    13566 pts/3 00:00:00 gdb1

    Em seguida, usamos o PID do processo (nesse caso, 13566) para dizer ao GDB qual o processo queremos debugar:

    gdb --pid 13566

    O GDB vai iniciar e tomar conta do processo a ser debugado, interrompendo a execução na próxima instrução. A partir desse ponto, podemos continuar com o processo de debug como estávamos fazendo anteriormente, não há distinção entre as duas sessões.

    Normalmente quando um bug aparece não estamos lá para ver, muito menos rodando o programa no GDB. Com sorte, às vezes temos disponível um core dump para ser analisado, e podemos trabalhar com a chamada análise post-mortem. Quando ocorre um erro de execução, por exempo, se o sistema estiver corretamente configurado, um core dump poderá ser gerado. Podemos também obter um core dump de um programa que está em execução ou de um programa que está sendo debugado.
    Escrevi o exemplo 3 para provocar um core dump devido à um sinal não manipulado. Se quisermos provocar um core dump, podemos usar o programa gcore com o PID do processo que queremos obter o core dump. A última opção é o comando gcore dentro do gdb, o que pode ser útil quando queremos um instantâneo da sessão para análise posterior.
    Vamos ver o que acontece quando executamos o programa de testes:

    # ./gdb2
    Segmentation fault (core dumped)

    Por padrão o core dump será gerado no diretório corrente no momento da geração do core, com o nome core.PID.
    Agora vamos instruir o GDB para carregar o core dump e os símbolos, supondo que o PID do processo que gerou o core dump seja 17247:

    # gdb --core=core.17247 --symbols=./gdb2
    Reading symbols from ./gdb2...done.
    Core was generated by `./gdb2'.
    Program terminated with signal 11, Segmentation fault.
    #0 0x00000000004005f2 in main (argc=1, argv=0x7fff35aad778) at main.c:40
    40 *bug = 0;

    Com isso já sabemos em qual linha foi gerada a falha. Podemos inspecionar e alterar os valores das variáveis, dos registradores, etc.

    Para saber mais como o core é gerado e como controlar a geração do arquivo, veja man 5 core.

    Dicas
    Os nomes dos comandos podem ser abreviados, desde que não entrem em conflito com outro comando. Por exemplo, para o comando disassemble, podemos digitar somente disas.

    Podemos fazer com que um comando seja sempre executado quando o processo for interrompido, usando display. O comando display é muito parecido com o p (print), mas será executado automaticamente cada vez que o programa debugado for paralisado.

    Outro ponto interessante é como o GDB interpreta e localiza o nome de um símbolo, já que em C são permitidas variáveis com o mesmo nome em escopos diferentes. O contexto atual é o da função que está em execução (ou melhor, a pilha atual). Não é possível referenciar variáveis locais em funções que não estão em execução nem fazem parte da pilha de chamadas, mas é possível especificar o contexto, usando ::. Por exemplo, considere o seguinte trecho de código num arquivo chamado teste.c:

    int my_test = 1234;
    
    int f_test(int my_test)
    {
    	return my_test * 2;
    }
    
    int main(int argc, char *argv[])
    {
    	int my_test;
    	for (i = 0; i < 100; i++)
    		print(hello world (%d)\n", my_test, f_test(my_test));
    	return 0;
    }
    

    Supondo um breakpoint em f_test, se quisermos ver o valor da variável global my_test, devemos

    p &'teste.c'::my_test

    Por outro lado, para referenciar a variável local my_test, dentro da função main, podemos

    p &main::my_test

    Se solicitarmos informações sobre my_test sem definir o escopo, veremos o my_test da pilha atual (o my_test declado dentro de f_test):

    p &my_test

    De dentro do GDB, digite help para obter ajuda ou a descrição resumida de algum comando, por exemplo, para obter um breve descrição do comando break:

    (gdb) help break
    Set breakpoint at specified line or function.
    break [LOCATION] [thread THREADNUM] [if CONDITION]
    LOCATION may be a line number, function name, or "*" and an address.
    If a line number is specified, break at start of code for that line.
    If a function is specified, break at start of code for that function.
    If an address is specified, break at that exact address.
    With no LOCATION, uses current execution address of selected stack frame.
    This is useful for breaking on return to a stack frame.
    THREADNUM is the number from "info threads".
    CONDITION is a boolean expression.
    Multiple breakpoints at one place are permitted, and useful if conditional.
    Do "help breakpoints" for info on other commands dealing with breakpoints.

    Antes de finalizar, três assuntos interessantes para pesquisa: variáveis de conveniência, scripts e debug remoto.

    Por último, se você ainda não descobriu, para sair do GDB:

    quit


    Multithreading no Linux - User mode

    Bom, suponho que nessa altura do campeonato todos estão muito bem informados sobre o que é multithread, convencidos sobre seus benefícios e conscientes dos problemas que enfrentamos quando fazemos uso dessa técnica. Se você ainda tem dúvidas, dê uma olhada aqui.

    No Linux podemos usar pthreads para implementação de multithreading. Pthreads (POSIX threads) é um padrão de implementação de multithreading, composto por uma lib (libpthread) e um header (pthread.h).

    Só para lembrar, comunicação entre threads requer métodos específicos que vão além do uso de simples variáveis compartilhadas. Mutexes e variáveis condicionais são dois desses métodos: mutexes servem para limitar a execução de um trecho de código a uma thread por vez e condicionais são usadas para sinalizar uma condição para outras threads.

    As estruturas básicas encontradas em pthread.h são:

    pthread_t identifica uma thread
    pthread_mutex_t tipo usado para manipular mutexes
    pthread_cond_t tipo usado para manipular conditions

    Além disso, declarada em time.h,
    struct timespec
    também é frequentemente usada.

    As funções essenciais da API são:

    pthread_create cria uma thread. Uma thread é simplesmente uma função que é executada independemente do fluxo do código que chama pthread_create.
    pthread_join aguarda que uma thread finalize. Não há uso de CPU enquanto a thread estiver em espera.
    pthread_mutex_init inicializa um mutex.
    pthread_mutex_destroy deve ser chamado quando um mutex não for mais usado.
    pthread_mutex_lock "pega" o mutex. Somente uma thread por vez consegue executar essa função. Se uma outra chamada ocorrer, a thread que chamou será colocada em espera. Não há uso de CPU enquanto a thread estiver em espera.
    pthread_mutex_unlock devolve o mutex. Se houver alguma thread esperando em pthread_mutex_lock, a thread que estiver esperando é liberada. Se houver mais de uma thread esperando, somente uma será liberada, mas não é possível determinar com antecedência qual delas.
    pthread_cond_init inicializa uma condition vatriable.
    pthread_cond_destroy deve ser chamado quando uma condition variable não for mais ser usada.
    pthread_cond_wait aguarda pela sinalização de uma condition. Não há uso de CPU enquanto a thread estiver em espera.
    pthread_cond_signal Libera uma, e somente uma, das threads aguardando pela condition em pthread_cond_wait.
    pthread_cond_broadcast Libera todas as threads que estiverem aguardando pela condition em pthread_cond_wait.

    Talvez seja mais fácil de entender estudando alguns exemplos.

    Exemplo 1
    Criando threads.
    Usando o projeto esqueleto, criei um programa bem simples que cria 10 threads que mostram na saída padrão um número, que também é usado no retorno da thread, aguarda até que todas finalizem, mostrando o resultado retornado por cada thread.

    Exemplo 2
    Erros provocados por falta de sincronização.
    Este programa apresenta comportamento imprevisível. Essa é uma das falhas mais traiçoeiras, porque o programa não "explode", ele até funciona, mas falha "inexplicavelmente" de vez em quando. Usando o projeto esqueleto criei um programa bem simples que demonstra como a utilização incorreta de multithreading pode causar dores de cabeça. Antes de entrar no maravilhoso mundo das múltiplas linhas de execução, porém, vou explicar porque tive usar variáveis qualificadas como volatile. Durante o processo de compilação, podemos instruir ao GCC, entre outras coisas, como queremos que o objeto criado a partir do código seja otimizado. Nesse exemplo estamos usando O3, o que significa que, entre outras coisas, o compilador pode eliminar um loop, se esse não houver efeitos colaterais. Por exemplo, ao encontrar

    int i;
    int j;
    for (j = 0, i = 0; i < 10; i++)
    	j++;
    

    o compilador pode otimizar para algo semelhante a

    i = 10;
    j = 10;
    

    desprezando o loop.

    Se fizermos questão que o loop seja executado, temos 2 opções:
    1) desligamos a otimização, usando -O0 (ou desligando somente a otimização que nos afeta).
    2) declaramos a variável como volatile, o que instrui o compilador a acessar o conteúdo da variável a cada acesso (ou, em português claro, não otimizar código em que a variável volatile estiver envolvida).
    Então, usando volatile, vamos forçar o loop a ser executado, fazendo o erro aparecer.

    Vamos supor que a CPU não consiga fazer uma soma de inteiros em uma única operação nem diretamente na memória, mas tenha que usar um armazenamento temporário em um registrador para fazer a soma e, só então, transferir o resultado de volta para a memória. Essa simples operação pode resultar em erro se estivermos em execução paralela. O erro ocorre porque, com as threads rodando em paralelo, pode ser que uma esteja somando o valor da variável e, antes que o valor seja atribuído à variável, outra thread altere o valor. Nesse caso, o valor do último cálculo será perdido, e o resultado não será o esperado. Talvez seja mais fácil entender usando uma figura:

    Exemplo 3
    Sincronizando o acesso do Exemplo 2 usando mutex.
    Uma das formas de consertar o defeito é utilizar um mutex, restringindo o acesso à um determinado trecho de código a uma única thread. Se alguma outra thread solicitar o ingresso no mesmo trecho, terá que esperar até a thread inicial "devolva" a permissão. A mesma figura anterior, agora com o mutex:

    Infelizmente, embora funcione corretamente, o programa vai executar muito mais lentamente. Frequentemente é possível melhorar o desempenho alterando a forma como o programa foi projetado. Assunto para um outro texto.

    Exemplo 4
    Este exemplo mostra como utilizar uma conditional variable para esperar a ocorrência de um evento, usando pthread_cond_signal, e seus efeitos quando muitas threads estão aguardando o mesmo evento.

    Exemplo 5
    Este exemplo mostra como utilizar uma conditional variable para esperar a ocorrência de um evento, usando pthread_cond_broadcast, e seus efeitos quando muitas threads estão aguardando o mesmo evento.

    Exemplo 6: TODO: deadlocks.


    Audio Streaming - Comunicação Serial

    Este texto faz parte da série sobre ALSA Streaming, apesar de não ter nada a ver com ALSA nem com streaming. É que a comunicação usando a porta serial serve para controlar o scanner Uniden, enviando e recebendo comandos a partir dele. Como ese foi o motivo dessa série...

    Para encontrar informações sobre o padrão que define o funcionamento do ponto de vista elétrico, pinagem, UART, etc, fora do escopo desse texto, consulte esse site, muito bom. Ou aqui, Wikipedia. Também fora do escopo do texto, informações sobre como o kernel trata a porta serial e tty em geral, consulte, no diretório $LINUX/Documentation, os arquivos serial/driver e tty.txt.

    Em user mode, o acesso à porta serial no Linux é feito através de um device file de caractere (oposto ao device file de bloco), e usa a estrutura de terminal. Apesar de poder aparecer em qualquer lugar, o device da porta serial normalmente está dentro do diretório /dev, com um nome que ttyS mais um dígito, geralmente indicando o número da porta. Por exemplo, /dev/ttyS0 para a primeira porta serial, /dev/ttyS1 para a segunda porta, e assim por diante (conversores USB/RS232 podem aparecer com nome diferente) . Basta abrir o device file, por exemplo, com open(), para transmitir/ler dados e através de IOCTLs podemos controlar os parâmetros de funcionamento da porta serial, como baud rate, paridade, etc., mas antes de entrar em detalhes, vamos ver as ferramentas existentes no sistema operacional que podem ser usadas para controlar, enviar e receber dados das portas seriais.

    Para Exemplo
    mandar uma string echo "hello world" > /dev/ttyS0
    mandar um arquivo cat file.txt > /dev/ttyS0
    ler e mostrar no terminal cat /dev/ttyS0
    mostrar a configuração stty -F /dev/ttyS0
    para configurar para 9600 8N1 stty -F /dev/ttyS0 9600 -clocal cread -crtscts cs8 -cstopb -parenb
    outras configurações específicas de porta serial setserial

    stty serve para controlar as características gerais de um terminal, incluindo eco local, interpretação de new line, etc, por isso algumas das opções de configuração parecem ter pouco a ver com uma porta serial e mais com o console.

    A libc fornece algumas funções para configurar terminais e também portas seriais. Quase todas as funções usam uma estrutura chamada termios:

    #define NCC 8
    struct termio {
            unsigned short c_iflag;         /* flags de entrada */
            unsigned short c_oflag;         /* flags de saída */
            unsigned short c_cflag;         /* flags de controle */
            unsigned short c_lflag;         /* flags de controle local */
            unsigned char c_line;           /* line discipline */
            unsigned char c_cc[NCC];        /* caracteres de controle */
    };
    

    A API:
    Todas as funções da API usam um file descriptor, daqueles retornados normalmente por "open".

    	/* preenche uma estrutura termios com a configuração do terminal indicado por fd */
           int tcgetattr(int fd, struct termios *termios_p);
    	/* configura o terminal indicado por fd de acordo com os parâmetros definidos na estrutura termios */
           int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
    	/* coloca o terminal em condição de break */
           int tcsendbreak(int fd, int duration);
    	/* aguarda até o buffer de saída ser transmitido */
           int tcdrain(int fd);
    	/* descarta tudo que estiver pendente tanto para recepção como para transmissão */
           int tcflush(int fd, int queue_selector);
    	/* usado quando o fluxo estiver sendo controlado por software, envia start/stop e/ou para/inicia a recepção */
           int tcflow(int fd, int action);
    

    As funções a seguir ajudam a manipular a estrutura termios.

    	/* configura termios para modo "raw": entrada caracter por caracter (ao contrário de linha a linha), eco local desligado, sem processamento especial de caracteres de controle:
               termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
               termios_p->c_oflag &= ~OPOST;
               termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
               termios_p->c_cflag &= ~(CSIZE | PARENB);
               termios_p->c_cflag |= CS8; */
           void cfmakeraw(struct termios *termios_p);
    	/*
             * retorna o baud rate codificado para entrada (cfgetispeed) ou saída (cfgetospeed).
             * RS232 usa o mesmo baud rate para entrada e saida.
             */
           speed_t cfgetispeed(const struct termios *termios_p);
           speed_t cfgetospeed(const struct termios *termios_p);
    	/* define o baud rate para entrada (cfsetispeed), saída (cfsetospeed) ou entrada e saída (cfsetspeed) */
           int cfsetispeed(struct termios *termios_p, speed_t speed);
           int cfsetospeed(struct termios *termios_p, speed_t speed);
           int cfsetspeed(struct termios *termios_p, speed_t speed);
    

    O baud rate em termios está codificado e não deve ser usado pelo seu valor absoluto, mas sim através das seguintes macros:

    Nome Baud Rate
    B0 Hang up
    B50 50
    B75 75
    B110 110
    B134 134
    B150 150
    B200 200
    B300 300
    B600 600
    B1200 1200
    B1800 1800
    B2400 2400
    B4800 4800
    B9600 9600
    B19200 19200
    B38400 38400
    B57600 57600
    B115200 115200
    B230400 230400

    Para mostrar como é fácil lidar com a porta serial no Linux, fiz um programinha de exemplo, usando o projeto esqueleto, que configura a porta serial, manda uma string e aguarda uma resposta por até 3 segundos, terminada em LINE FEED.

    No próximo texto vou implementar o protocolo completo do BCT-8.


    Debug - User mode - 1

    Nessa série de textos vou tentar mostrar algumas técnicas/programas básicos para mapear o funcionamento e identificar problemas em programas executando em modo de usuário, ou, para alguns, debugar. Existem várias técnicas e situações diferentes, e para cada caso existe uma ou mais ferramentas.
    Vou começar então pelas ferramentas disponíveis no Linux que podem ajudar nessa tarefa.

    /proc
    O /proc contém muitas informações valiosíssimas sobre o estado do sistema, dos drivers e de cada processo em execução. As informações sobre um determinado processo estão em um diretório do /proc cujo nome coincide com o PID do processo. De acordo com $LINUX/Documentation/filesystem/procfs.txt e os fontes em $LINUX/fs/procfs/, as entradas dentro do diretório de um processo em /proc são (no 2.6.31):

    Entrada Tipo Descrição
    attr diretório Atributos relacionados à extensão de segurança (SELinux, AppArmor, etc)
    auxv somente leitura O aux vector do processo
    cgroup somente leitura Control group
    clear_refs somente escrita Limpa o campo "Referenced" do smaps, para ajudar a monitorar
    cmdline somente leitura Argumentos da linha de comando
    coredump_filter leitura/escrita Determina/verifica quais páginas de memória serão gravadas em disco em caso de coredump.
    cpuset somente leitura Quais CPUs estão disponíveis para esse processo (/=todas)
    cwd link para diretório link para o diretório atual
    environ somente leitura Valores das variáveis de ambiente
    exe link para arquivo aponta para o arquivo executável. Pode ser inválido se o processo foi criado pelo kernel.
    fd diretório Contém todos os file descriptors, em forma de link para o arquivo real. Mostra também sockets e pipes.
    fdinfo diretório Contém informações sobre o estado de cada file descriptor (2.6.31: flags de abertura/criação e posição do cursor para próxima operação de IO).
    io somente leitura Informações sobre IO (leitura, escrita, etc).
    latency somente leitura Ajuda a descobrir onde o processo está gastando mais tempo
    limits somente leitura Valores limite (nice, memória, penging signals, etc)
    loginuid leitura/escrita id do usuário owner do processo
    maps somente leitura Mostra onde estão carregados as dependências e os executáveis, além dos atributos de cada seção.
    mem leitura/escrita Memória em uso
    mountinfo somente leitura Informações adicionais sobre filesystems montados
    mounts somente leitura Filesystems montados visíveis
    mountstats somente leitura subset de mounts, formatado
    net diretório Estatísticas e informações relacionadas à rede.
    numa_maps somente leitura Páginas alocadas por node e memory policy
    oom_adj leitura/escrita Ajusta o score para escolha do processo a ser matado se o kernel estive em condição de pouca memória
    oom_score somente leitura Score para escolha do processo a ser matado se o kernel estive em condição de pouca memória
    pagemap somente leitura Estado da paginação de memória (para cada página no espaço de endereçamento)
    personality somente leitura ABI em uso, veja os valores em $LINUX/include/linux/personality.h (0x00000000=Linux)
    root link Link para o diretório que esse processo considera como root
    sched somente leitura Info do scheduler
    schedstat somente leitura Mais info do scheduler
    sessionid somente leitura o task->session_id, usado em audit.
    smaps somente leitura extensão de maps, mostra dados de utilização de memória de cada mapeamento.
    stack somente leitura Stack trace
    stat somente leitura Informações diversas, consulte a função do_task_stat em $LINUX/fs/proc/array.c
    statm somente leitura Estatísticas de memória, no 2.6.31: size, resident, shared, text, lib, data, NULL
    status somente leitura Quase o mesmo que stat, mas formatado.
    syscall somente leitura Mostra os parâmetros do último syscall. O primeiro ítem é o número do syscall, consulte $LINUX/include/asm-generic/unistd.h
    task diretório Informações de cada thread
    wchan somente leitura Caso o processo esteja em sleep, o símbolo ou o endereço da função onde está

    ldd
    quase todos os programas usam bibliotecas. Sem entrar no mérito, o fato é que muitas vezes o programa não funciona, ou porque não consegue resolver suas dependências de biblioteca dinâmica ou porque carrega a biblioteca incorreta. ldd nos ajuda a descobrir quais são as bibliotecas dependentes e as versões que serão carregadas quando um programa for executado. Entretanto, ldd não vai mostrar as bibliotecas carregadas por demanda, ou seja, somente quando o programa decide carregá-las, sem que a dependência seja explícita. Por exemplo:

    $ ldd /sbin/echo
    linux-vdso.so.1 => (0x00007fff9ffa8000)
    libc.so.6 => /lib64/libc.so.6 (0x00000034d0400000)
    /lib64/ld-linux-x86-64.so.2 (0x00000034d0000000)

    significa que o programa echo depende das bibliotecas linux-vdso.so.1, libc.so.6 e /lib64/ld-linux-x86-64.so.2 para ser executado.

    Outro caso:

    $ ldd ./zvent
    linux-gate.so.1 => (0x00d0c000)
    liblotes.so => not found
    libpthread.so.0 => /lib/libpthread.so.0 (0x00a63000)
    libresolv.so.2 => /lib/libresolv.so.2 (0x00269000)
    libdl.so.2 => /lib/libdl.so.2 (0x00a30000)
    libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00a7f000)
    libm.so.6 => /lib/libm.so.6 (0x00a37000)
    libc.so.6 => /lib/libc.so.6 (0x008b9000)
    libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x00ba6000)
    /lib/ld-linux.so.2 (0x00897000)

    Nesse caso, a dependência liblotes.so não pode ser resolvida, e o programa provavelmente não vai carregar.

    Usando o /proc para inspecionar um processo em execução podemos ver que as libs efetivamente carregadas nem sempre são as reportadas por ldd. Faça uma experiência com um processo complexo, como firefox, pidgin ou java, por exemplo. Supondo que você conheça o PID de um processo e queira ver o que o ldd reporta como dependência:

    $ ldd /proc/${THE_PID}/exe

    Para saber quais são as libs efetivamente mapedas:

    $ cat /proc/${THE_PID}/maps | sed -ne 's/[^\/]*\(.*\.so\)/\1/p' | sort -u

    ps
    Comando simples, mas muito útil, ajuda a responder como, quando, por quem e quais recursos estão sendo usados por um processo. Alguns exemplos:

    ps -C ps Mostra todos as instâncias atualmente em execução do cujo executável tem o nome "ps"
    ps -U 1234 Mostra os processos associados ao userid 1234
    ps -fp 1 Mostra os detalhes do processo init
    ps -Af Lista detalhes de todos os processos em execução

    kill
    kill envia um signal para um processo, cada signal tem uma função diferente, alguns deles são interpretados de forma específica por um programa específico. O kill é usado para simplesmente "matar" um processo, mas ele pode fazer mais. Para saber quais os signals disponívels, execute

    $ kill -l

    Esta é uma tabela com os principais signals (SIGTERM é o default do kill se você não especificar qual o signal).

    Signal Descrição
    SIGHUP Hangup (POSIX).
    SIGINT Interrupt (ANSI).
    SIGQUIT Quit (POSIX).
    SIGILL Illegal instruction (ANSI).
    SIGTRAP Trace trap (POSIX).
    SIGABRT Abort (ANSI).
    SIGIOT IOT trap (4.2 BSD).
    SIGBUS BUS error (4.2 BSD).
    SIGFPE Floating-point exception (ANSI).
    SIGKILL Kill, unblockable (POSIX).
    SIGUSR1 User-defined signal 1 (POSIX).
    SIGSEGV Segmentation violation (ANSI).
    SIGUSR2 User-defined signal 2 (POSIX).
    SIGPIPE Broken pipe (POSIX).
    SIGALRM Alarm clock (POSIX).
    SIGTERM Termination (ANSI).
    SIGSTKFLT Stack fault.
    SIGCLD Same as SIGCHLD (System V).
    SIGCHLD Child status has changed (POSIX).
    SIGCONT Continue (POSIX).
    SIGSTOP Stop, unblockable (POSIX).
    SIGTSTP Keyboard stop (POSIX).
    SIGTTIN Background read from tty (POSIX).
    SIGTTOU Background write to tty (POSIX).
    SIGURG Urgent condition on socket (4.2 BSD).
    SIGXCPU CPU limit exceeded (4.2 BSD).
    SIGXFSZ File size limit exceeded (4.2 BSD).
    SIGVTALRM Virtual alarm clock (4.2 BSD).
    SIGPROF Profiling alarm clock (4.2 BSD).
    SIGWINCH Window size change (4.3 BSD, Sun).
    SIGPOLL Pollable event occurred (System V) - Mesmo que SIGIO.
    SIGIO I/O now possible (4.2 BSD).
    SIGPWR Power failure restart (System V).
    SIGSYS Bad system call.

    Se tudo o que você quer é matar o processo de qualquer jeito, tente

    $ kill -9 PID_DO_PROCESSO

    Se isso não funcionar, só boot.

    lsof
    lsof é fantástico: ele mostra quais são os arquivos que estão abertos no sistema todo ou por um determinado processo (ou ainda usando outros critérios). O básico do lsof:

    Exemplo Descrição
    lsof Lista todos os arquivos abertos por todos os processos (pode haver restrições de acesso, para ver tudo somente como root)
    lsof -p 1 Lista os arquivos abertos pelo processo 1 (init)

    strace
    Esse é um comando poderosíssimo, e muito útil: ele monitora as chamadas de sistema (syscalls) feitas por um processo. Ele é capaz de decodificar a chamada e a resposta, mostrando os dados em forma simbólica, quando é o caso (flags, por exemplo, são mostrados por nome ao invés de um número). Ele ainda pode monitorar os processos-filho criados pelo processo monitorado.
    Veja um exemplo:

    $ strace echo "hello world"
    execve("/bin/echo", ["echo", "hello world"], [/* 46 vars */]) = 0
    brk(0)                                  = 0x1a08000
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f23e5fbd000
    access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
    open("/etc/ld.so.cache", O_RDONLY)      = 3
    fstat(3, {st_mode=S_IFREG|0644, st_size=143645, ...}) = 0
    mmap(NULL, 143645, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f23e5f99000
    close(3)                                = 0
    open("/lib64/libc.so.6", O_RDONLY)      = 3
    read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000\354A\3204\0\0\0"..., 832) = 832
    fstat(3, {st_mode=S_IFREG|0755, st_size=2404376, ...}) = 0
    mmap(0x34d0400000, 3635368, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x34d0400000
    mprotect(0x34d056f000, 2093056, PROT_NONE) = 0
    mmap(0x34d076e000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x16e000) = 0x34d076e000
    mmap(0x34d0773000, 18600, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x34d0773000
    close(3)                                = 0
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f23e5f98000
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f23e5f97000
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f23e5f96000
    arch_prctl(ARCH_SET_FS, 0x7f23e5f97700) = 0
    mprotect(0x34d076e000, 16384, PROT_READ) = 0
    mprotect(0x34d021d000, 4096, PROT_READ) = 0
    munmap(0x7f23e5f99000, 143645)          = 0
    brk(0)                                  = 0x1a08000
    brk(0x1a29000)                          = 0x1a29000
    open("/usr/lib/locale/locale-archive", O_RDONLY) = 3
    fstat(3, {st_mode=S_IFREG|0644, st_size=98765792, ...}) = 0
    mmap(NULL, 98765792, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f23e0165000
    close(3)                                = 0
    fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f23e5fbc000
    write(1, "hello world\n", 12hello world)           = 12
    close(1)                                = 0
    munmap(0x7f23e5fbc000, 4096)            = 0
    close(2)                                = 0
    exit_group(0)                           = ?
    

    top
    O top mostra uma lista de processos classificados de acordo com a utilização dos recursos do sistema. Ao contrário do ps, que é um instantâneo do estado do sistema, top monitora continuamente as processos em execução. É útil se você quer, por exemplo, monitorar picos de utilização de CPU ou memória e descobrir os responsáveis. Além disso, top pode matar um processo ou modificar sua prioridade (um assunto para outra hora). top é um programa interativo, você pode controlá-lo através de teclas de atalho. A mais importante é 'q' para sair :)
    Aviso: top consome CPU ;-)
    Por exemplo, execute o top, tecle 's' e digite 0.1 - refresh em .1 segundos. O top irá aparecer... no topo, ou quase lá.

    Nos próximos textos, não perca o espetacular gdb e o sensacional valgrind.