Como funciona o pânico em Rust

Como funciona o Rust Panic


O que exatamente acontece quando você chama panic!() ?
Recentemente, passei muito tempo estudando as partes da biblioteca padrão associadas a isso, e a resposta é bastante complicada!


Não consegui encontrar documentos explicando a imagem geral do pânico no Rust, então vale a pena anotar.


(Artigo vergonhoso: O motivo pelo qual me interessei neste tópico é que o @Aaron1011 implementou o suporte para desenrolamento de pilha no Miri.


Eu queria ver isso em Miri desde tempos imemoriais, e nunca tive tempo para implementá-lo, então foi realmente ótimo ver como alguém simplesmente envia PR para apoiar isso do nada.


Após várias rodadas de verificação do código, ele foi injetado recentemente.


Ainda existem algumas arestas , mas o básico está bem definido.)


O objetivo deste artigo é documentar a estrutura de alto nível e as interfaces relacionadas que entram em jogo no lado do Rust.


O mecanismo real de desenrolamento de pilhas é uma questão completamente diferente (da qual não estou autorizado a falar).


Nota: este artigo descreve o pânico desse commit .


Muitas das interfaces descritas aqui são internas instáveis ​​do libstd e podem mudar a qualquer momento.


Estrutura de alto nível


Tentando entender como o pânico funciona enquanto lê o código no libstd, você pode facilmente se perder no labirinto.
Existem vários níveis de indireção que são conectados apenas pelo vinculador,
existe #[panic_handler] e um "runtime panic handler" (controlado pela estratégia de pânico definida por -C panic ) e "panic traps" , e acontece que o pânico no contexto de #[no_std] exige um caminho de código completamente diferente ... muito muita coisa acontecendo.


Pior ainda, a RFC que descreve armadilhas de pânico os chama de "manipulador de pânico", mas o termo foi redefinido desde então.


Eu acho que o melhor lugar para começar é com interfaces que controlam duas direções:


  • O manipulador de pânico de tempo de execução é usado pelo libstd para controlar o que acontece depois que as informações de pânico são impressas no stderr.
    Isso é determinado pela estratégia de pânico: ou interrompemos ( -C panic=abort ) ou iniciamos o desenrolamento da pilha ( -C panic=unwind ).
    (A manipulação do pânico em tempo de execução também fornece uma implementação para catch_unwind , mas não falaremos sobre isso aqui.)


  • O manipulador de pânico é usado pelo libcore para implementar (a) o pânico inserido pela geração de código (como pânico causado por estouro aritmético ou indexação de array / fatia fora dos limites) e (b) core::panic! macro (esta é uma macro de panic! na própria libcore e no contexto #[no_std] ).



Ambas as interfaces são implementadas através de blocos extern : listd / libcore, respectivamente, simplesmente importam algumas funções que eles delegam e, em algum outro lugar na árvore de caixas, essa função é implementada.


A importação é permitida apenas durante a ligação; Observando localmente o código, não se pode dizer onde mora a implementação real da interface correspondente.
Não é de surpreender que eu tenha me perdido várias vezes ao longo do caminho.


No futuro, essas duas interfaces serão muito úteis; quando você erra. A primeira coisa a verificar é se você confundiu o manipulador de pânico e o manipulador de pânico em tempo de execução .
(E lembre-se de que também existem interceptadores de pânico, chegaremos a eles.)
Isso acontece comigo o tempo todo.


Além disso, core::panic! e std::panic! não é o mesmo; como veremos, eles usam caminhos de código completamente diferentes.


libcore e libstd implementam sua própria maneira de causar pânico:


  • core::panic! O libcore é muito pequeno: delega imediatamente o pânico ao manipulador .


  • libstd std::panic! (O panic! "normal" panic! in Rust) lança um mecanismo de pânico totalmente funcional que fornece uma interceptação de pânico controlada pelo usuário.
    O gancho padrão exibirá uma mensagem de pânico no stderr.
    Após a conclusão da função de interceptação, a libstd a delega ao manipulador de pânico em tempo de execução .


    A libstd também fornece um manipulador de pânico que chama o mesmo mecanismo, então core::panic! também termina aqui.



Vamos agora examinar essas partes com mais detalhes.


Lidar com pânico durante a execução do programa


A interface para o panic runtime (representado por este RFC ) é a função __rust_start_panic(payload: usize) -> u32 que é importada pelo libstd e posteriormente resolvida pelo vinculador.


O argumento usize aqui é realmente *mut &mut dyn core::panic::BoxMeUp - é aqui que *mut &mut dyn core::panic::BoxMeUp “dados úteis” do pânico (informações disponíveis quando são detectadas).


BoxMeUp é um detalhe instável da implementação interna, mas, olhando para esse tipo, vemos que tudo o que realmente faz é quebrar o dyn Any + Send , que é o tipo de dados de pânico úteis retornados por catch_unwind e thread::spawn .


BoxMeUp::box_me_up retorna Box<dyn Any + Send> , mas como um ponteiro bruto (já que Box não Box disponível no contexto em que esse tipo está definido); BoxMeUp::get apenas empresta o conteúdo.


Duas implementações dessa interface são fornecidas em libpanic_unwind : libpanic_unwind para -C panic=unwind (padrão na maioria das plataformas) e libpanic_abort para -C panic=abort .


std::panic!


No topo da interface do panic runtime , o libstd implementa o mecanismo padrão do Rust panic no módulo interno std::panicking .


rust_panic_with_hook


A principal função através da qual quase tudo acontece é rust_panic_with_hook :


 fn rust_panic_with_hook( payload: &mut dyn BoxMeUp, message: Option<&fmt::Arguments<'_>>, file_line_col: &(&str, u32, u32), ) -> ! 

Esta função aceita a localização da origem do pânico, uma mensagem não formatada opcional (consulte a documentação fmt::Arguments ) e dados úteis.


Sua principal tarefa é desencadear o que é o atual interceptor de pânico.
Os interceptores de pânico têm o argumento PanicInfo , por isso precisamos da localização da fonte de pânico, das informações de formato da mensagem de pânico e de dados úteis.
Isso corresponde muito bem ao argumento rust_panic_with_hook !
file_line_col e message podem ser usados ​​diretamente para os dois primeiros elementos; payload transforma em &(dyn Any + Send) através da interface BoxMeUp .


Curiosamente, o interceptor de pânico padrão ignora completamente a message ; o que você vê é converter a carga útil para &str ou String (não importa o que funcione).
Presumivelmente, o chamador deve se certificar de que a formatação da message , se presente, produz o mesmo resultado.
(E os que discutimos abaixo garantem isso.)


Por fim, rust_panic_with_hook enviado para o manipulador de pânico do tempo de execução atual.


No momento, apenas a payload ainda é relevante - e o que é importante: message (com vida útil de '_ indica que links de curta duração podem estar contidos, mas dados úteis de pânico se propagam pela pilha e, portanto, com vida útil 'static ).


A 'static restrição 'static está bem escondida, mas depois de um tempo percebi que Any significa 'static (e lembre-se de que o dyn BoxMeUp usado apenas para obter o Box<dyn Any + Send> ).


Pontos de entrada Libstd


rust_panic_with_hook é uma função privada para std::panicking ; o módulo fornece três pontos de entrada no topo dessa função central e um que a ignora:


  • A implementação padrão do manipulador de pânico que suporta (como veremos) o pânico do core::panic! e pânico interno (de estouro aritmético ou indexação de array / fatia).
    Obtém PanicInfo como entrada, e deve transformar isso em argumentos para rust_panic_with_hook .
    Curiosamente, embora os componentes PanicInfo e os argumentos rust_panic_with_hook bastante semelhantes, e parece que eles podem ser simplesmente encaminhados, não é.
    Em vez disso, libstd ignora completamente o componente de payload do PanicInfo e define a payload real (passada para rust_panic_with_hook ) para que ele contenha uma message .


    Em particular, isso significa que o manipulador de pânico em tempo de execução não importa para aplicativos no_std .
    Ele só entra em jogo quando a implementação do manipulador de pânico no libstd é usada.
    ( A estratégia de pânico escolhida por meio do -C panic ainda é importante, pois também afeta a geração de código.
    Por exemplo, com -C panic=abort código pode se tornar mais simples, pois você não precisa suportar o desenrolamento da pilha).


  • begin_panic_fmt , suportando a versão do std::panic! (ou seja, isso é usado quando você passa vários argumentos para uma macro).
    Basicamente, basta PanicInfo argumentos da string de formato no PanicInfo (com cargas úteis ) e chamar os manipuladores de pânico padrão que acabamos de discutir.


  • begin_panic suportando a std::panic! com std::panic! .
    Curiosamente, isso usa um caminho de código completamente diferente dos outros dois pontos de entrada!
    Em particular, este é o único ponto de entrada que permite transferir dados úteis arbitrários .
    Essa carga útil é simplesmente Box<dyn Any + Send> para que possa ser passada para rust_panic_with_hook , e é isso.


    Em particular, um interceptador de pânico que analisa o campo de message de PanicData não poderá ver a mensagem em std::panic!("do panic") , mas pode ver a mensagem em std::panic!("panic with data: {}", data) pois o último passa por begin_panic_fmt .
    Isso parece incrível. (Mas observe também que PanicData::message() ainda não está estável.)


  • update_count_then_panic acabou sendo estranho: esse ponto de entrada suporta resume_unwind e, na verdade, não causa interceptação de pânico.
    Em vez disso, ele é imediatamente enviado ao manipulador de pânico.
    Por exemplo, begin_panic permite ao chamador selecionar dados úteis arbitrários.
    Diferentemente do begin_panic , a função de chamada é responsável por compactar e dimensionar a carga útil; A função update_count_then_panic simplesmente encaminha seus argumentos quase literalmente para o manipulador de pânico em tempo de execução.



Manipulador de pânico


std::panic! O mecanismo é realmente útil, mas requer colocar dados no heap através do Box , que nem sempre está disponível.
Para dar à libcore uma maneira de causar pânico, os manipuladores de pânico foram introduzidos.
Como vimos, se o libstd estiver disponível, ele fornecerá uma implementação dessa interface core::panic! entre em pânico nas visualizações libstd.


A interface para o manipulador de pânico é a função fn panic(info: &core::panic::PanicInfo) -> ! importações libcore, e isso é resolvido posteriormente pelo vinculador.
O tipo PanicInfo é o mesmo dos interceptadores de pânico: contém o local da fonte de pânico, uma mensagem de pânico e dados úteis ( dyn Any + Send ).
A mensagem de pânico é apresentada no formato fmt::Arguments , ou seja, uma string de formato com argumentos que ainda não foram formatados.


core::panic!


Além da interface do processador de pânico, a libcore fornece uma API de pânico mínima .
core::panic! a macro cria fmt::Arguments que é passada ao manipulador de pânico .
A formatação não ocorre aqui, pois isso exigirá alocação de memória no heap; É por isso que PanicInfo contém uma string de formato " PanicInfo " com seus argumentos.


Curiosamente, o campo de payload do PanicInfo passado para o manipulador de pânico, sempre definido como um valor fictício .
Isso explica por que o manipulador de pânico da libstd ignora os dados da carga útil (e cria novos dados de carga útil a partir da message ), mas me faz pensar por que esse campo faz parte da API do manipulador de pânico.
Outra conseqüência disso é que core::panic!("message") e std::panic!("message") (opções sem formatação) realmente levam a pânico muito diferente: a primeira se transforma em fmt::Arguments , passados ​​pela interface do manipulador de pânico e libstd cria dados String úteis, formatando-os.
Este último, no entanto, usa diretamente &str como dados úteis, e o campo de message permanece None (como já mencionado).


Alguns elementos da API de pânico no libcore são elementos de linguagem porque o compilador insere chamadas para essas funções durante a geração de código:


  • Um elemento panic é chamado quando o compilador precisa causar um pânico que não requer formatação (por exemplo, estouro aritmético); esta é a mesma função que também suporta core::panic! com um argumento core::panic! .
  • panic_bounds_check é chamado quando uma verificação de limite de matriz / fatia falha; chama o mesmo método que core::panic! com formatação .

Conclusões


Passamos por quatro níveis de API, dois dos quais foram redirecionados por meio de chamadas de função importadas e resolvidos pelo vinculador.
Que jornada!
Mas chegamos ao fim.
Espero que você não tenha entrado em pânico ao longo do caminho. ;)


Mencionei algumas coisas como surpreendentes.
Acontece que todos eles estão conectados ao fato de que interceptores e processadores de pânico compartilham a estrutura PanicInfo em sua interface, que contém uma message e payload formatada opcionalmente com um tipo apagado:


  • Um interceptador de pânico sempre pode encontrar uma mensagem já formatada na payload , de modo que a message parece inútil para os interceptadores.De fato, a message pode não estar presente mesmo que a payload contenha uma mensagem (por exemplo, para std::panic!("message") ).
  • O manipulador de pânico nunca receberá payload , portanto o campo parece inútil para os manipuladores.

Lendo o RFC a partir da descrição do manipulador de pânico , parece que o plano era para o core::panic! também suportam dados úteis arbitrários, mas até agora isso não se materializou.
No entanto, mesmo com essa extensão futura, acho que temos uma invariância de que quando a message é Some , então a payload == &NoPayload (portanto, dados úteis são redundantes) ou a payload é uma mensagem formatada (portanto, a mensagem é redundante).


Gostaria de saber se existe um caso em que ambos os campos serão úteis e, se não, podemos codificar isso, tornando-os duas variantes de enum ?


Provavelmente, existem boas razões contra essa proposta para o design atual; seria ótimo colocá-los em algum lugar no formato de documentação. :)


Há muito mais a dizer, mas neste momento eu convido você a seguir os links para o código-fonte que incluí acima.


Com uma estrutura de alto nível em mente, você deve conseguir seguir este código.
Se as pessoas pensassem que essa revisão deveria ser colocada em algum lugar para sempre, eu ficaria feliz em transformar este artigo em um blog em algum tipo de documentação - embora eu não tenha certeza se esse seria um bom lugar.


E se você encontrar algum erro no que escrevi, entre em contato!

Source: https://habr.com/ru/post/pt482358/


All Articles