Sobrescrevendo um executável em execução ou .so

4

Eu tenho uma pergunta sobre como substituir um executável em execução ou sobrescrever um arquivo de biblioteca compartilhada (.so) que está sendo usado por um ou mais programas em execução.

De volta ao dia, pelas razões óbvias, substituir um executável em execução não funcionou. Existe até um valor errno específico, ETXTBSY, que cobre este caso.

Mas há algum tempo, tenho notado que, ao tentar acidentalmente substituir um executável em execução (por exemplo, disparando uma compilação cuja última etapa é cc -o exefile em um exefile que está em execução ), funciona!

Então, minhas perguntas são, como isso funciona, é documentado em qualquer lugar, e é seguro depender disso?

Parece que alguém pode ter alterado ld para desvincular seu arquivo de saída e criar um novo, apenas para eliminar erros nesse caso. Eu não posso dizer se está fazendo isso o tempo todo, ou apenas se for necessário (ou seja, talvez depois que ele tentar substituir o arquivo existente e encontrar o ETXTBSY). E não vejo nenhuma menção a isso na página de manual do ld . (E eu me pergunto por que as pessoas não estão reclamando que ld agora pode estar quebrando seus hard links, ou alterando a propriedade de arquivos, e assim.)

Adendo: A questão não era especificamente sobre cc / ld (embora isso acabe sendo uma grande parte da resposta); A questão era realmente apenas "Como é que eu nunca mais vejo ETXTBSY? Ainda é um erro?" E a resposta é, sim, ainda é um erro, apenas um raro na prática. (Veja também a resposta esclarecedora que acabei de postar na minha própria pergunta.)

    
por Steve Summit 14.11.2017 / 18:48

4 respostas

3

Depende do kernel, e em alguns kernels pode depender do tipo de executável, mas acho que todos os sistemas modernos retornam ETXTBSY (” arquivo de texto ocupado") se você tentar abrir um executável em execução para escrever ou executar um arquivo que está aberto para gravação. A documentação sugere que sempre foi o caso no BSD , mas não foi o caso no início O Solaris ( versões posteriores implementou essa proteção ), que corresponde à minha memória. Tem sido o caso no Linux desde sempre, ou pelo menos 1,0 .

O que vai para executáveis pode ou não ir também para bibliotecas dinâmicas. Sobrescrever uma biblioteca dinâmica causa exatamente o mesmo problema que sobrescreve um executável: as instruções serão subitamente carregadas do mesmo endereço antigo no novo arquivo, o que provavelmente tem algo completamente diferente. Mas este não é o caso em todos os lugares. Em particular, no Linux, os programas chamam a chamada de sistema open para abrir uma biblioteca dinâmica, com os mesmos sinalizadores de qualquer arquivo de dados, e o Linux felizmente permite reescrever o arquivo de biblioteca mesmo que um processo em execução possa carregar código dele a qualquer momento.

A maioria dos kernels permite remover e renomear arquivos enquanto eles estão sendo executados, assim como eles permitem remover e renomear arquivos enquanto eles estão abertos para leitura ou escrita. Assim como um arquivo aberto, um arquivo que é removido enquanto está sendo executado não será realmente removido da mídia de armazenamento enquanto estiver em uso, ou seja, até a última instância do executável sair. Linux e * BSD permitem isso, mas o Solaris e o HP-UX não.

Remover um arquivo e gravar um novo arquivo com o mesmo nome é perfeitamente seguro: a associação entre o código a carregar e o arquivo aberto (ou executado) que contém o código passa pelo descritor de arquivo, não pelo nome do arquivo . Ele tem o benefício adicional de poder ser feito atomicamente, gravando em um arquivo temporário e movendo esse arquivo (a chamada do sistema rename substitui atomicamente um arquivo de destino existente pelo arquivo de origem). É muito melhor que remove-then-open-write, uma vez que não coloca temporariamente um executável inválido, parcialmente escrito, no lugar

Se cc e ld sobrescreverem o arquivo de saída, ou removê-lo e criar um novo, depende da implementação. GCC (pelo menos versões modernas) e Clang fazem isso, em ambos os casos, chamando unlink no destino, se existir, então open para criar um novo arquivo. (Eu me pergunto por que eles não fazem write-to-temp-renomear.)

Eu não recomendo depender desse comportamento, exceto como uma proteção, pois ele não funciona em todos os sistemas (pode funcionar em todos os sistemas modernos para executáveis, mas não em bibliotecas compartilhadas), e os toolchains comuns não fazem coisas da melhor maneira. Em seus scripts de construção, sempre gere arquivos em um arquivo temporário e, em seguida, mova-os para o lugar, a menos que você saiba que a ferramenta subjacente faz isso.

    
por 14.11.2017 / 23:42
2

O que geralmente acontece se um arquivo é removido enquanto ele tem um identificador de arquivo aberto é que ele está marcado para exclusão assim que o último identificador de arquivo é fechado. O arquivo não aparecerá mais nas listagens de diretório (por exemplo), mas aparecerá em e. g. lsof output, marcado como um arquivo excluído, mas em uso.

A saída de lsof abaixo é ajustada para brevidade e clareza:

$ cat - >> foo &
[1] 30779
$ lsof | grep 30779
cat       30779                  ghoti    1w      REG      252,0        0    262155 /home/ghoti/foo

[1]+  Stopped                 cat - >> foo
$ rm foo
$ ls foo
ls: cannot access 'foo': No such file or directory
$ lsof | grep 30779
cat       30779                  ghoti    1w      REG      252,0        0    262155 /home/ghoti/foo (deleted)

Se eu fg , ainda posso escrever para o (excluído) foo com o cat ainda em execução (e, na verdade, posso recuperar o arquivo de /proc/30779/fd/1 se precisar, contanto que eu faça isso enquanto cat ainda tiver o arquivo aberto ).

    
por 14.11.2017 / 18:57
0

Seu palpite está correto: ld escreve um novo arquivo, que você pode verificar usando ls -li . A opção -i mostra o número do inode, que muda após cada compilação. Eu não acho que muitos se importem em quebrar links rígidos; programas geralmente não são compilados para que o executável seja gravado em seu destino final por ld.

    
por 14.11.2017 / 19:32
0

Responder minha própria pergunta (ou, na verdade, esclarecer um pouco a questão e depois explicar a resposta à pergunta esclarecida):

Minha pergunta era: "Eu nunca mais vejo o ETXTBSY. Ainda existe um erro? Ou os kernels modernos permitem que você sobrescreva os executáveis sem reclamar e (de alguma forma) sem quebrar os executáveis em execução?"

Eu estava começando a suspeitar seriamente que os kernels modernos estavam implementando algum tipo de semântica sofisticada de copy-on-write ao escrever para executar executáveis.

Mas não foi isso. ETXTBSY ainda é definitivamente um erro.

A resposta para minha confusão é simplesmente que gravações em executáveis dificilmente surgem na prática. Se você colocar um novo executável no lugar (e o antigo ainda estiver em execução), você quase nunca irá sobrescrevê-lo; você está sempre removendo e substituindo. Se você está usando mv , você está removendo e substituindo. Se você estiver usando install , ou dpkg -i , ou algo assim, você está removendo e substituindo. Somente se por algum motivo você tentou usar cp você estaria tentando sobrescrevê-lo, e correndo o risco de obter ETXTBSY se o antigo ainda estivesse rodando.

E depois, devido à mudança silenciosa para ld , tentar cc -o sobre um executável em execução agora está na categoria de "remover e substituir" também.

    
por 17.11.2017 / 17:32