quinta-feira, 17 de janeiro de 2013

Escrevendo meu próprio Sistema Operacional (ou quase...)



Uma das coisas mais frustrantes para os estudantes de engenharia/ciência da computação são as aulas de Sistemas Operacionais. Você aprende a criar threads, pipes, schedulers e tudo mais, mas não aprende aquilo que tanto sonhava: como escrever seu próprio sistema operacional.

Bom, não vou explicar aqui como criar um Windows ou Linux. Para chegar perto isso, prepare-se para alguns anos de desenvolvimento. Mas vou mostrar os passos básicos, isto é, como escrever um Hello World básico para rodar no boot da máquina, independente de Windows, Linux, Solaris, BSD ou seja lá qual for o SO que você use.

Primeiramente, precisamos escrever um programa em Assembly puro, sem dependência nenhuma de nenhum sistema operacional. Estou utilizando o compilador NASM como exemplo, que pode ser baixado gratuitamente.

(Não vou entrar no mérito de ensinar Assembly aqui, até porque não sou um especialista na linguagem. Se quiser se aprofundar mais, existem ótimos tutoriais na net.)

O seguinte código imprime um “Ola Mundo” na tela, sem utilizar nenhuma API ou qualquer outra dependência de um Sistema Operacional:

;os.asm
jmp 0x7C0:main

main:

mov ax, 0x07C0 ;a MBR eh carregada pela bios em 0x7C0
mov ds, ax
mov es, ax
mov si, olamundo
call print_string ;chama a funcao que imprime o "Ola Mundo"
mainloop:
jmp mainloop
olamundo db 'Ola Mundo :P', 0x0D, 0x0A, 0

print_string:
lodsb ;pega o proximo byte de SI
or al, al ; compara o registrador com ele mesmo.
jz .done ; Se o resultado for zero, cai fora
mov ah, 0x0E
int 0x10 ; Imprime o caracter na tela
jmp print_string
.done:
ret


Para compilá-lo:

nasm os.asm -f bin -o os.bin

Ótimo, temos o código compilado, agora precisamos copiá-lo para o setor de boot da máquina. A menos que você queira que seu computador não faça mais nada além de mostrar a mensagem “Ola Mundo”, você não vai querer copiar isso para o setor de boot do seu HD.

Algumas opções são:
-Copiar para um pendrive e, para testar, modificar o boot da BIOS para bootar no pendrive;
-Utilizar uma máquina virtual.

Para ficar mais emocionante, vamos copiar para um pendrive (depois da brincadeira, você precisará formatá-lo para poder voltar a usá-lo normalmente, então guarde seus arquivos importantes).

Se você estiver no Linux, pode copiá-lo diretamente utilizando o comando dd:

dd if=os.bin of=/dev/fd#

Sendo # o número do seu dispositivo de pendrive. (Este comando talvez não funcione, pois algumas BIOS exigem que se coloque os bytes 0x55aa no fim do setor de boot).

No Windows, podemos escrever um programinha em C++ para realizar esta cópia. Você poderá escrever um programa similar para o Linux também, caso o comando acima não funcione.

Primeiramente, precisamos saber qual é o valor do dispositivo que acessa o pendrive. Não adianta tentar apontar para “d:\” ou algo do tipo, pois este endereço aponta para a partição fat dentro do pendrive. Queremos apontar para a MBR.

Para isso, clique com o botão direito em Meu Computador, vá em Gerenciamento e então em Gerenciamento de Disco (Disk Management).
Embaixo, você deve ter alguma coisa do tipo:

Disk 0 - C:
Disk 1 - D:

Veja qual é o disco referente ao seu Pendrive (no meu caso é Disk 2). Aí você pode apontar para ela pelo caminho:

\\.\PhysicalDrive#

(sendo # o número do disco).

O código do programa para copiar o seu código em ASM para a MBR é o seguinte:



#include <cstdio>
#include <cstdlib>
#include <io.h>
#include <windows.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
  FILE* arqAsm = NULL;

  unsigned char buf[512]; //A MBR tem tamanho 512

  arqAsm = fopen( "os.bin", "r" );

  fseek( arqAsm, 0, 0 );

  fread( (void*)buf, 1, 510, arqAsm ); //lemos o conteúdo do arquivo para nosso buffer

  buf[510] = 0x55; //colocamos o 0x55aa no final da MBR, pois
  buf[511] = 0xaa; //algumas BIOS precisam disso


  fclose( arqAsm );
  //aqui abrimos a MBR do pendrive
  HANDLE hFile;

  hFile = CreateFile("\\\\.\\PhysicalDrive2", //Apontando para "Disk 2". Mude para o que você for usar
  GENERIC_WRITE, // abrindo para escrita
  0,
  NULL,
  OPEN_EXISTING,
  NULL,
  NULL);

  if (hFile == INVALID_HANDLE_VALUE)
  {
    printf("Problema abrindo a MBR");
    return 0;
  }

  SetFilePointer( hFile, 0, 0, FILE_BEGIN );

  DWORD escritos;
  if ( !WriteFile( hFile, buf, 512, &escritos, NULL ) )
  {
    printf("Problema ao escrever na MBR" );
  }

  CloseHandle( hFile );

  return 0;
}



Antes de executá-lo, verifique novamente se você colocou o endereço do disco correto! Se você apontou para o seu HD, não vai mais conseguir bootar o seu Windows!

Se você quiser escrever o programa em Linux, em vez da função “CreateFile” utilize “open”, e em vez de “WriteFile” utilize “write” (note que os parametros são diferentes).

Agora é só compilar e executar. Note que, em algumas versões do Windows, será necessário executá-lo como administrador.

Agora basta reiniciar a máquina, entrar na BIOS, colocar o pendrive como boot #1 e ver o seu “Hello World” rodar!

É isso pessoal, em um próximo post eu explico com mais detalhes como a MBR funciona.


Agora você já pode sobrescrever a MBR do computador da sua namorada com uma mensagem do tipo “Eu te amo, minha linda!”. Ela vai adorar.

domingo, 6 de novembro de 2011

Ordem de inicialização dos atributos de um objeto

Diz a lenda que o seguinte código dá um erro em tempo de execução:

#include <cstring>

class Foo
{
public:
  Foo ( int size ) :
    mSize(size),
    mValues( new char[mSize] )
    {
      memset( mValues, 0, mSize );
    }
  ~Foo ()
  {
    delete[] mValues;
  }

private:
  char* mValues;
  const int mSize;
};

int main()
{
  Foo deu(10);
  return 0;
}


Você sabe dizer qual é?

O problema aqui está na alocação de memória para mValues. Observe o construtor:

Foo ( int size ) :
  mSize(size),
  mValues( new char[mSize] )
  {
    memset( mValues, 0, mSize );
  }

Aparentemente está ok, não é? No exemplo acima, o objeto é criado passando 10 como parâmetro. Sendo assim, o valor 10 é atribuído para mSize, e depois mValues é alocado com o tamanho de mSize, e então é feito um memset para zerar todos os valores de mValues.
Muito bonito, mas o problema está na ordem de inicialização das variáveis. Quem define esta ordem é a ordem em que as variáveis foram declaradas dentro da classe.

Note:

private:
  char* mValues;
  const int mSize;


Não importa se você vai chamar

Foo ( int size ) :
  mSize(size),
  mValues( new char[mSize] )
{
  ...
}

ou

Foo ( int size ) :
  mValues( new char[mSize] ),
  mSize(size)
{
  ...
}
que mValues sempre vai ser inicializado antes de mSize.

Para resolver o problema?
Inverta a ordem de declaração das variáveis.

O seguinte código:

#include <cstring>

class Foo
{
public:
  Foo ( int size ) :
    mSize(size),
    mValues( new char[mSize] )
  {
    memset( mValues, 0, mSize );
  }
  ~Foo ()
  {
    delete[] mValues;
  }

private:
  const int mSize;
  char* mValues;
};

int main()
{
  Foo deu(10);
  return 0;
}

Não apresenta problema algum.

Dizem que é uma boa prática inicializar as variáveis no construtor na mesma ordem que elas foram declaradas na classe para evitar este tipo de dúvida. Se você tiver paciência para fazê-lo, mande bala! :)

sábado, 22 de outubro de 2011

Hello World alternativo

#include <cstdio>

class HelloWorld
{
public:
    HelloWorld()
    {
        printf("Hello World!");
    }
    ~HelloWorld(){}
};

HelloWorld obj;

int main()
{
    return 0;
}

Signed/Unsigned Mismatch

Dando manutenção em códigos antigos ao longo da vida, percebi que os warnings do tipo “signed/unsigned mismatch” são extremamente comuns. Acredito que isso se deva ao fato de que situações de comparação entre variáveis signed e unsigned acontecem com frequência, e como estes warnings parecem inofensivos, ninguém lhes dá atenção.

Mas não é bem assim. Todo warning deve ser levado a sério.

Observe os seguintes exemplos:

Ex. 1:


int x = -1;
unsigned int y = 10U;

if ( x > y )
{
   printf("X eh maior");
}
else
{
   printf("Y eh maior");
}

Ex. 2:

signed char x = -1;
unsigned char y = 10U;

if ( x > y )
{
   printf("X eh maior");
}
else
{
   printf("Y eh maior");
}


Os dois exemplos são praticamente iguais. A única diferença é que, no primeiro, foram utilizadas variáveis do tipo int. No segundo, foram utilizadas variáveis do tipo char.

Rodando os exemplos em um PC, obtemos os seguintes resultados:

Ex. 1:
X eh maior

Ex. 2:
Y eh maior

Mas como???

Bom, vamos lá.
Quando você faz uma operação qualquer (soma, subtração, comparação...), a regra diz que a operação será feita em uma base com pelo menos o tamanho do maior dos operandos. Em uma mesma base, o tipo unsigned é considerado maior que o tipo signed.
Observe a tabela abaixo:

Operação
Base em que o cálculo será realizado
int32 + int16
int32 ou maior.
int8 + uint8
uint8 ou maior.
(int16)int8 == uint8
int16 ou maior.
int16 - int16
int16 ou maior.

Agora voltemos aos exemplos acima.
No segundo caso, foi feita uma comparação entre um signed char e um unsigned char. Estes tipos tem 8 bits em um PC normal (pode variar em alguns raros sistemas embarcados), portanto o compilador é livre para resolver a operação na base 8 bits ou em uma maior.

Por questões de desempenho, o compilador optou por realizar esta comparação na base int. Isto porque um processador de 32 bits realiza uma operação na base 32 mais rápido do que na base 8 (acredito que um processador 64 bits teria realizado a operação na base 64, mas não cheguei a testar isso).

Como (int)-1 é menor que (int)10, o resultado da operação foi que Y é maior.

Agora, no primeiro exemplo, foi feita a comparação entre um int e um unsigned int. Como o unsigned tem preferência sobre o tipo signed, o compilador precisava realizar a operação em uma base de tamanho maior ou igual a unsigned int. Como não haveria nenhum ganho de desempenho se a operação fosse realizada em uma base maior que esta, a operação foi realizada em unsigned int.

Assim, o valor (int)-1 foi convertido para (unsigned int)-1, que vale 0xFFFFFFFF (considerando que o int tenha tamanho 32) ou 0xFFFFFFFFFFFFFFFF (em plataforma 64 bits).

Em ambos os casos, o valor de X será maior que o de Y, causando este comportamento estranho.


Bom, muito interessante. Se você não está convencido ainda, vou mostrar um exemplo prático de um problema que eu tive uma vez por causa destas comparações incorretas.

Em uma empresa X que eu trabalhei há muito tempo atrás, havia um código em produção que, de vez em quando, imprimia um monte de lixo na tela e travava. Este problema acontecia muito raramente, era difícil de se reproduzir e nunca foi encontrado nada de errado no código que pudesse causar este tipo erro. Para ajudar, alguns meses antes havia sido feito um trabalho bem-sucedido para remover todos os warnings do fonte. Desta forma, o código compilava bonito, com zero erros e zero warnings. Concluiu-se, então, que o problema devia ser de hardware.

Até que, um dia, mexendo em alguma coisa nada a ver, observei a seguinte linha de código:

for (unsigned int x = 0U; x < (unsigned int)str.GetLen(); ++x )

Aparentemente ok, mas aquele cast para unsigned int me deixou com uma pulga atrás da orelha. Olhei o histórico deste código no SVN e vi que, antes da Operação Zero Warnings, o código era da seguinte forma:

for (unsigned int x = 0U; x < str.GetLen(); ++x )

Este código faz exatamente o que o novo faz, porém, ao ser compilado, dá um alerta dizendo "signed/unsigned mismatch on line XXX" que, em outras palavras, é o compilador dizendo "Cuidado, você pode estar fazendo cagada".

Ao colocar aquele cast, o que o desenvolvedor fez foi dizer ao compilador "Tá bom, eu sei que eu tô fazendo cagada, não me encha mais o saco". Se este desenvolvedor tivesse prestado atenção no warning, em vez de simplesmente tentar removê-lo de qualquer forma, teria resolvido o bug milenar.


O problema é que a variável str era do tipo MyString, uma classe que era definida da seguinte forma:

class MyString
{
public:
  int GetLen()
  {
    int retorno = -1;
    if ( mpDados != NULL )
    {
      retorno = (int)strlen( mpDados );
    }
    return retorno;
  }
...
private:
  char* mpDados;
...
};

Olha só! A GetLen() retorna -1 quando mpDados aponta para NULL!
Isso fazia com que o loop fosse executado 4294967295 vezes quando GetLen() retornasse -1, fazendo com que um monte de lixo fosse impresso na tela, até o código travar de vez.

O correto, nesta situação, seria mudar o tipo de "x" para int, em vez de converter o retorno de GetLen() para unsigned.

Por isso, da próxima vez, olhem seus warnings com mais atenção :)

(P.s.: Sim, o cast de strlen para int também não é muito bacana, mas isso já é outra história...) .

terça-feira, 27 de setembro de 2011

Duff's Device

Estou postando este artigo a título de curiosidade. Embora você ainda possa encontrar uma situação prática hoje em dia em que o Duff's Device possa ser útil, em 99,9% das situações seu benefício é desprezível.

O Duff's Device é uma técnica criada por Tom Duff em 1983 para fazer uma cópia serial de dados (a mesma coisa que o memcpy faz) com um desempenho maior. Foi amplamente utilizada por progamadores de Assembly e programadores de C no começo dos tempos.

Basicamente, a maneira mais rápida de se copiar dados de uma região de memória para outra, em C, é:

void meu_memcpy( unsigned char* destino, unsigned char* origem, int quantidade )
{
     while( quantidade-- > 0 )
     {
         *destino++ = *origem++;
     }
}

O problema com esta solução é que temos que verificar se quantidade é maior que zero em cada iteração. Se estivermos copiando mil bytes, haverá mil comparações.
A solução encontrada por Duff foi diminuir este número de comparações, copiando os elementos de 8 em 8. Depois de copiar 8 elementos, ele fazia as devidas verificações e partia para o próximo bloco de cópia.
O algoritmo original do Duff era:

void meu_memcpy( unsigned char* destino, unsigned char* origem, int quantidade )
{
     register int n = (quantidade + 7) / 8;
     switch(quantidade % 8)
     {
         case 0: do { *destino++ = *origem++;
         case 7: *destino++ = *origem++;
         case 6: *destino++ = *origem++;
         case 5: *destino++ = *origem++;
         case 4: *destino++ = *origem++;
         case 3: *destino++ = *origem++;
         case 2: *destino++ = *origem++;
         case 1: *destino++ = *origem++;
             } while(--n > 0);
     }
}

Basicamente n é a quantidade de oitos que cabem em quantidade. Se der número quebrado, arredonda para cima. Ou seja, se quantidade vale 8, n vale 1. Se quantidade vale 10, n vale 2.
O switch pega o resto da divisão de quantidade por 8 para que esta quantidade seja copiada na primeira iteração. Nas demais, serão copiados 8 bytes.

Você deve ter estranhado o formato do do...while no meio dos cases, mas este código é perfeitamente válido em C.
Talvez você esteja se perguntando também porque foi usado o valor 8, e não outro qualquer.
A explicação disso é que os processadores calculam mais rapidamente qualquer divisão por valores 2^n.
Por exemplo, se quiser dividir um número por 2, basta rotacioná-lo para a direita 1 bit. Para dividir por 4, rotacione 2 bits. Para dividir por 8, 3 bits, e assim por diante.

O Duff ' Device teve sua vantagem no passado, mas hoje em dia são pouquíssimas as situações em que se vale a pena aplicá-lo. Primeiro que o código tem uma complexidade razoável perto do original. Um leigo com certeza terá dificuldades em entendê-lo.
Segundo que não necessariamente este código será mais rápido. Os compiladores atuais executam vários tipos de otimizações. Se o ganho de desempenho for justificável na sua aplicação para que você venha a adotar esta implementação, é interessante que você faça testes com o memcpy original e com a sua implementação do Duff's Device para ver se, na prática, ela realmente é mais rápida.

domingo, 25 de setembro de 2011

Aventuras com Heranças

Embora a herança em si seja um tema básico da orientação a objetos, ela possui uma série de artimanhas e regras (principalmente para quem programa em C++) que devem ser seguidas para não cair em futuras armadilhas.
Basicamente, existem três tipos de heranças em C++: a pública, a protegida e a privada. O primeiro tipo é o mais conhecido. Os outros dois não são muito utilizados e raramente são ensinados na faculdade ou nos cursos de C++. Além disso, a herança pode ser virtual ou não.
Vou explicar cada um destes tipos na sequência:

Herança pública:

De longe a mais conhecida e mais utilizada (embora muitas vezes utilizada de forma incorreta). A regra mais importante para esta herança é: quando você diz que B herda de A, você está dizendo que B é um A. Tenha isso em mente. Sempre. Só herde uma classe de outra se você puder dizer, no bom português, que a classe filha é um tipo da classe mãe. Nunca herde com o objetivo de economizar código ou coisa do tipo.
Por exemplo, você pode herdar Carro de Veiculo, Moto de Veiculo, Ferrari de Carro, Marte de Planeta, Carrefour de Supermercado, mas nunca deve herdar Moto de Carro, por exemplo. Mesmo que no seu código seja tentador fazer isso, pois a maioria das funções de Moto estão implementadas em Carro, não faça tal herança. O ideal é criar uma classe acima de Carro e Moto (como Veiculo) e colocar lá as funções em comum.
Vou citar um exemplo de um código que eu trabalhei há algum tempo que fazia o uso incorreto de herança pública.
Tínhamos uma classe chamada Thread, com um método virtual puro chamado Run, um Inicia e um SetDelay. Basicamente, se você queria ter uma thread no sistema, bastava herdar desta classe, implementar o método Run, e chamar o Inicia para que o conteúdo de Run ficasse sendo executado de acordo com o delay definido.
Como o código implementava uma comunicação por Gprs e esta comunicação deveria ficar sendo executada em uma thread separada no sistema, fizemos Gprs herdar de Thread. O problema é que Gprs não é uma Thread! Gprs pode ser considerado uma Rede, um Canal de Comunicação, mas definitivamente não é uma thread!
Apesar de parecer inofensiva, a decisão de herdar Gprs de Thread começou a causar dores de cabeça. O problema é que no código você acaba tendo algo do tipo:

Gprs* objGprs = new Gprs();
objGprs->SetDelay( 1000 );
objGprs->Inicia();

Agora, para uma pessoa que nunca mexeu no código, o que o Inicia faz? A pessoa pode imaginar que vai configurar IP, zerar as variáveis da classe, ligar o hardware do modem, o que for, mas não vai imaginar que este comando está iniciando a Thread.
Além disso, tínhamos a necessidade de ter uma função que iniciasse o modem e configurasse a rede, mas esta função não poderia se chamar Inicia porque o Inicia já está implementado em Thread.
Assim, o código acabou ficando uma beleza do tipo:

Gprs* objGprs = new Gprs();
objGprs->IniciaGprs( IP, PORTA );
objGprs->SetDelay( 1000 );
objGprs->Inicia();

O que beira a estupidez.

O correto, neste caso, seria ter criado duas classes: uma chamada ThreadGprs que herda de Thread, e outra chamada Gprs que não herda de ninguém (ou herda de CanalComunicacao, ou algo do tipo), e fazer com que ThreadGprs agregasse Gprs. Um código do tipo

ThreadGprs* objThreadGprs = new ThreadGprs();
objThreadGprs->SetDelay( 1000 );
objThreadGprs->Inicia();

Faz sentido pra qualquer novato que bata o olho nele.

Bom, dito isso, vamos ao que interessa: o que é uma herança pública?

O funcionamento é simples: a classe filha vai conseguir acessar os atributos e métodos públicos e protegidos da mãe, e as outras classes vão conseguir acessar os atributos e métodos públicos da filha e da mãe através de um objeto da filha.

Veja o exemplo:

class Base
{
public:
    void FuncaoUm()
    {
        printf( "Funcao numero 1\n" );
    }

    virtual void FuncaoDois()
    {
        printf( "Funcao numero 2 da base\n" );
    }
};

class Derivada: public Base
{
public:
    void FuncaoDois()
    {
        printf( "Funcao numero 2 da derivada!\n" );
    }
};

int main()
{
    Base* obj = new Derivada();

    obj->FuncaoUm();
    obj->FuncaoDois();

    delete obj;
    return 0;
}

O resultado disso vai ser:

Funcao numero 1
Funcao numero 2 da derivada!

(Se você não entende o que a palavra virtual faz, experimente rodar o código novamente sem ela e verá que o comportamento será diferente do esperado. Via de regra, sempre que você pretende reescrever uma função na derivada, declare a função da Base como virtual).

Função virtual pura

Se você quer obrigar a Derivada a reimplementar uma função, você pode declará-la como virtual pura. Esta função pode ou não ser implementada na Base, mas a Derivada deve obrigatoriamente implementá-la.
Uma função virtual pura é declarada da seguinte forma:

virtual void Funcao() = 0;

Veja o exemplo:

class Base
{
public:    
    //Função virtual pura não implementada.
    virtual void FuncaoPuraUm() = 0;

    //Função virtual pura implementada:
    virtual void FuncaoPuraDois() = 0
    {
        printf( "Funcao Pura Dois na base\n" );
    }
};

class Derivada: public Base
{
public:
    //Somos obrigados a implementas as funções
    //virtuais puras da base aqui, senão dá erro de
    //compilação.
    void FuncaoPuraUm()
    {
        printf( "Funcao Pura Um implementada na derivada\n" );
    }

    //Apesar de estar implementada na base, precisamos
    //implementá-la aqui também.
    void FuncaoPuraDois()
    {
        printf( "Funcao Pura Dois implementada na derivada\n" );

        //opcionalmente podemos chamar a implementação
        //da base aqui.
        Base::FuncaoPuraDois();
    }
};

int main()
{
    Base* obj = new Derivada();

    obj->FuncaoPuraUm();
    obj->FuncaoPuraDois();

    delete obj;
    return 0;
}

O resultado vai ser:

Funcao Pura Um implementada na derivada
Funcao Pura Dois implementada na derivada
Funcao Pura Dois na base

Tranquilo, né? Se você seguir corretamente a regra que eu comentei no começo, vai tirar um grande benefício com heranças, e dificilmente vai se meter em problemas.

Vamos para os tipos menos conhecidos:


Herança privada:

Um tipo de herança menos conhecido e que deve ser evitado. Diferente da pública, na herança privada, quando você diz que B herda de A, está dizendo que B tem A. Por exemplo, Carro poderia fazer uma herança privada de Motor.

A principal diferença entre esta herança e a pública, do ponto de vista técnico, é que quem possui um objeto da classe Derivada não irá conseguir chamar, por este objeto, uma função da classe Base.

Voltemos ao primeiro exemplo:

class Base
{
public:
    //Função virtual pura não implementada.
    virtual void FuncaoPuraUm() = 0;

    //Função virtual pura implementada:
    virtual void FuncaoPuraDois() = 0
    {
        printf("Funcao Pura Dois na base\n");
    }
};

class Derivada: private Base
{
public:
    //Somos obrigados a implementas as funções
    //virtuais puras da base aqui, senão dá erro de
    //compilação.
    void FuncaoPuraUm()
    {
        printf( "Funcao Pura Um implementada na derivada\n" );
    }

    //Apesar de estar implementada na base, precisamos
    //implementá-la aqui também.
    void FuncaoPuraDois()
    {
        printf( "Funcao Pura Dois implementada na derivada\n" );
        //opcionalmente podemos chamar a implementação
        //da base aqui.
        Base::FuncaoPuraDois();
    }
};

int main()
{
    //Base* obj = new Derivada();
    Derivada* obj = new Derivada();

    //obj->FuncaoPuraUm();
    obj->FuncaoPuraDois();

    delete obj;
    return 0;
}

Note as diferenças:

  1. A herança agora é private, e não mais public;
  2. Fazer um ponteiro do tipo Base apontar para um objeto alocado como o tipo Derivada já não funciona mais. (É possível utilizar um reinterpret_cast aqui para passar esta segurança, porém você já está hackeando o código e vai acabar com um comportamento inesperado mais cedo ou mais tarde);
  3. Não é mais possível chamar as funções que estão na Base através do objeto da Derivada. Assim, a linha obj->FuncaoPuraUm(); teve que ser comentada.

O mesmo vale caso você herde (independente da forma da herança) uma classe de Derivada (uma classe chamada DerivadaDaDerivada, vamos supor). DerivadaDaDerivada não conseguirá acessar nenhum método ou atributo de Base diretamente, independente se este método ou atributo é público, protegido ou privado.

Mas por que usar herança privada, e não composição ou agregação?

Difícil responder, até porque você deve utilizar composição ou agregação em vez de herança privada.
Existem duas razões para você optar por herança privada:

  1. Caso você precise que a sua classe derivada acesse os métodos e atributos protegidos da base. Isso não seria possível caso fosse utilizado composição ou agregação, a menos que você declarasse a classe agregada como friend da classe agregadora.
  2. Caso você queira declarar na base uma função virtual (tanto faz se é pura ou não) e queira que ela seja reimplementada na derivada.
Sinceramente não consigo pensar em nenhum exemplo prático bom para se utilizar este tipo de herança.
Uma situação, talvez, fosse no exemplo do Gprs que eu comentei acima. Fazer Gprs herdar de forma privada de Thread seria melhor do que fazer a herança pública, porque, desta forma, quem chamasse:

objGprs->Inicia();

Estaria, com certeza, chamando a Inicia do Gprs, e não da Thread. A diferença é que daí a responsabilidade de inicializar a thread, definir timeouts e tudo mais ficaria a cargo da classe Gprs.


Herança Protegida

Dos três tipos de herança, este é o pior. Ninguém sabe ao certo o que significa dizer que B faz uma herança protegida de A, mas está perto de ser um relacionamento do tipo B tem A, como na herança privada.

O funcionamento deste tipo de herança é bem semelhante ao da (herança) privada. Os métodos da Base continuam inacessíveis por quem tenta acessá-los por um objeto da Derivada. A diferença está em se você herdar de Derivada (o exemplo da DerivadaDaDerivada). Neste caso, a DerivadaDaDerivada conseguirá acessar os métodos e atributos públicos e protegidos da Base sem problemas.

Herança Virtual

Os três tipos citados acima podem ser virtuais ou não (embora para a herança privada eu não veja sentido).
As heranças virtuais são úteis para o caso das heranças do tipo diamante (quem programa em Java treme ao ouvir isso).
Uma herança do tipo diamante se forma quando você tem uma Base, duas (ou mais) derivadas e depois uma classe que faz herança múltipla dessas duas (ou mais) derivadas. Pense o seguinte:

class Base
{
protected:
    int x;
};

class DerivadaUm: public Base
{
};

class DerivadaDois: public Base
{
};

class Join: public DerivadaUm, public DerivadaDois
{
public:
    void Funcao()
    {
        x = 0;
    }
};

Isso vai dar um erro de compilação, dizendo que o acesso a “x” é ambíguo. Isso acontece porque Join enxerga dois x: um que veio de Base por meio da DerivadaUm e outro que veio de Base por DerivadaDois.

Para que este código funcione, é necessário que DerivadaUm e DerivadaDois façam herança virtual de Base.
Assim, o código fica da seguinte forma:

class Base
{
protected:
    int x;
};

class DerivadaUm: virtual public Base
{
};

class DerivadaDois: virtual public Base
{
};

class Join: public DerivadaUm, public DerivadaDois
{
public:
    void Funcao()
    {
        x = 0;
    }
};

Isso criará apenas uma instância de x para Join.

Bom, dizem que heranças do tipo virtual devem ser evitadas. O problema não está com as heranças virtuais, mas sim com a herança do tipo diamante. Se você chegar a um modelo que chegue a este tipo de configuração, evite-o. Modele de forma diferente. Mas, inevitavelmente, você vai acabar tendo que dar manutenção em um código que está mal modelado, e talvez esta seja a sua única saída...