É possível (se entediante) escrever código de máquina direto. Talvez você escreva o programa no assembler em um pedaço de papel e depois o traduza manualmente nas instruções numéricas do código da máquina que você digita na memória da máquina. Você pode até pular a etapa do montador em papel se tiver memorizado os valores numéricos de todas as instruções de código de máquina - não é incomum naqueles dias, acredite ou não!
Os primeiros computadores foram diretamente programados em binário, alternando os comutadores físicos. Foi uma grande melhoria de produtividade quando o hardware evoluiu para permitir que o programador (ou o assistente de entrada de dados) insira o código em números hexadecimais via teclado!
Um assembler de software só se tornou relevante quando mais memória se tornou disponível (já que o código do assembler ocupa mais espaço do que o código bruto da máquina) e o hardware evoluiu para permitir entrada alfanumérica. Portanto, os primeiros montadores foram escritos diretamente por pessoas fluentes em código de máquina.
Quando você tem um assembler, você pode escrever um compilador para uma linguagem de nível superior no assembler.
A história para C tem várias etapas. O primeiro compilador C foi escrito em B (um predecessor de C), que por sua vez foi escrito em BCPL. BCPL é uma linguagem bastante simples (por exemplo, não possui tipos), mas ainda é um passo em frente ao assembler bruto. Então você vê como linguagens gradualmente mais complexas são construídas em linguagens mais simples, desde o assembler. E o próprio C é uma linguagem bem pequena e simples para os padrões atuais.
Hoje, o primeiro compilador para um novo idioma geralmente é escrito em C, mas quando o idioma atinge uma certa maturidade, é frequentemente reescrito "em si". O primeiro compilador Java foi escrito em C, mas posteriormente reescrito em Java. O primeiro compilador C # foi escrito em C ++, mas recentemente foi reescrito em C #. O compilador / intérprete Python é escrito em C, mas o projeto PyPy é uma tentativa de reescrevê-lo no Python.
Nem sempre é possível escrever um compilador / intérprete para um idioma no próprio idioma. Existe um intérprete JavaScript escrito em JavaScript, mas os compiladores / intérpretes nos navegadores atuais ainda são gravados em C ou C ++ por motivos de desempenho. JavaScript escrito em JavaScript é simplesmente muito lento.
Mas você não precisa usar C como o "idioma inicial" para um compilador. O primeiro compilador de F # foi escrito em OCaml, que é o outro idioma que está mais relacionado ao F #. Quando o compilador foi concluído, ele foi reescrito em F #. O primeiro compilador para Perl 6 foi escrito em Haskell (uma linguagem funcional pura muito diferente do Perl), mas agora possui um compilador escrito em C.
Um caso interessante é o Rust, onde o primeiro compilador foi escrito no OCaml (agora é reescrito no Rust). Isso é notável porque o OCaml geralmente é considerado um nível mais alto que o Rust, que é uma linguagem de sistemas mais próxima do metal. Portanto, nem sempre os idiomas de nível superior são implementados nos idiomas de nível inferior; também pode ser o contrário.