AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / user-13081625

Juliean's questions

Martin Hope
Juliean
Asked: 2024-10-16 01:00:06 +0800 CST

Verificações de nulos implícitos para chamadas de função

  • 7

Verificações de nulos implícitos são uma técnica para remover verificações explícitas para referências/pontos nulos em uma representação nativa de linguagens de alto nível e, em vez disso, confiar no processador emitindo uma violação de acesso, que é então manipulada (ou seja, SEH) e traduzida em uma exceção gerenciada. É usada principalmente em casos em que a sobrecarga de manipulação de exceções é secundária, por exemplo, se sabemos que exceções nulas são raras.

Em todos os exemplos que encontrei, essas verificações são feitas para instruções que fazem um acesso ao ptr em questão:

int m(Foo foo) {
  return foo.x;
}

Aqui, poderíamos simplesmente emitir o código asm:

mov rax,[rcx]

E fazer com que o mecanismo nativo de tratamento de exceções lide com a geração de uma NullReferenceException, em vez de travar.

Mas e quanto às chamadas de função?

int m(Foo foo) {
  return foo.MemberFunction();
}

É possível usar verificações de nulos implícitas lá também? Estou interessado especificamente em x64-asm. Parece mais difícil lá. Vamos dar uma olhada em uma chamada de função não virtual exemplar em asm (o código não corresponde à função 1:1, ele contém um "mov" apenas para mostrar que um objeto é configurado no registro usado para uma chamada de função de membro no Windows):

mov     rcx,[rsp+20h]           // load target-object from stack-local (Foo*)
call    Foo::MemberFunction     // call Foo::MemberFunction, can be represented with an address w/o fixups of the ptr 

Aqui, não temos nenhum acesso à memória para a qual "rcx" aponta. Então, se por definição da linguagem, tal chamada deve lançar uma NullReferenceException no call-site, precisaríamos usar verificações explícitas:

mov     rcx,[rsp+32h]           // load target-object from stack-local (Foo*)
test    rcx,rcx
je      .L0                     // exception-handler already moved out of hot-path
call    Foo::MemberFunction     // call Foo::MemberFunction, can be represented with an address w/o fixups of the ptr 

...
.L0:
call throwNullReferenceException();

Ou existe alguma maneira mais eficiente de substituir o par test+je por uma instrução, que gera uma violação de acesso? Eu estava pensando que poderia fazer

mov     rcx,[rsp+32h]           // load target-object from stack-local (Foo*)
mov     rax,[rcx]               // mov into unused reg, to trigger access-violation
call    Foo::MemberFunction     // call Foo::MemberFunction, can be represented with an address w/o fixups of the ptr 

Isso não usaria nenhuma ramificação e não exigiria uma chamada adicional para uma invocação de exceção. No entanto, ele potencialmente precisaria ler a memória de [rcx], o que não é necessário no outro método. Como isso funciona em comparação com a ramificação? Se for pior, há alguma maneira que seja melhor? Veja abaixo para mais explicações do caso de uso completo.

Fundo

Tenho uma linguagem de alto nível personalizada, que é compilada para bytecode e, em seguida, para ASM nativo. A linguagem lida com verificações de nulos graciosamente com exceções de NullReference. Exceções ainda são sempre erros que precisam ser abordados , e não algo que ocorre normalmente. Portanto, o código para lidar com exceções pode ser ineficiente. O importante é que o código seja executado o mais rápido possível, dado o caso comum de nenhuma exceção (portanto, nenhuma referência nula). É por isso que verificações de nulos implícitas parecem atraentes. Remover todas as ramificações e código adicional necessário para lidar com a exceção para chamadas pode ser benéfico. No entanto, mesmo as verificações existentes já devem ser rápidas. A ramificação deve ser bem previsível para ser sempre falsa, e eu já fiz isso para que este caso não exija um jmp, mas tenha o código executado linearmente (o que eu li ser mais otimizado).

Então, dado isso, minha tentativa de me livrar desses cheques no caso que mencionei é tola ou há alguma maneira de fazer isso de forma otimizada?

windows
  • 1 respostas
  • 45 Views
Martin Hope
Juliean
Asked: 2024-03-08 19:02:55 +0800 CST

CALL com um endereço de retorno modificado

  • 10

Qual é a melhor maneira de lidar com uma CALL em assembly x64, que deve retornar para um endereço de retorno ligeiramente alterado? Principalmente no que diz respeito à eficiência/velocidade de execução. Vou explicar brevemente o que estou tentando fazer.

Fundo

Eu tenho uma linguagem de script visual interpretada e personalizada, que é compilada em código nativo. Esta linguagem possui corrotinas baseadas em pilha integradas e, anteriormente, elas ainda eram tratadas semi-interpretadas (com uma classe de pilha separada para armazenar os dados da corrotina). Estou no processo de nativizá-lo totalmente, para que apenas o RSP seja usado.

Uma parte dessas corrotinas é a capacidade de rendimento aninhado, ou seja, se uma corrotina chamar outro método de rendimento, esse método pode render internamente para suspender toda a invocação. Essas informações são tratadas por meio de uma estrutura "YieldState", armazenada em um registro. Isso significa que, para a nova variante totalmente nativa, podemos apenas chamar um método de rendimento de uma corrotina com uma instrução de chamada:

call      12345; // [rip+12345] => yieldingMethod

Pelo menos, em teoria. Como nossas corrotinas são baseadas em pilha, armazenamos variáveis ​​locais claramente na pilha, e não em algum tipo de classe como as corrotinas sem pilha fariam. Isso requer que a limpeza (caso a corrotina seja destruída antes de terminar) seja feita por meio de outro método, que chamei de "manipulador de interrupção". A invocação desse manipulador de interrupção é bastante comum em meu caso de uso prático, mas não excessivamente. Portanto, meu objetivo era fornecer algo que fosse mais rápido que um manipulador de exceções (que geralmente requer alguma pesquisa global do quadro), mas não exigisse a configuração explícita desse endereço para cada chamada. Então o que fiz foi incorporar o endereço do manipulador de interrupção entre a chamada e o endereço de retorno - como para a versão antiga do código tínhamos que carregar o retorno manualmente, isso não era um problema:

lea rcx,[rip+25]; // 25 is the assumed byte-size up until the return address
mov rdx,rbx;      // load non-native call stack
call prepareMethodYielding; // stores return-address on stack
jmp 12345;        // actually call our "yieldingMethod"
mov r15,interruptAddress;

A última instrução nunca é executada - deixamos o endereço de retorno para ignorá-la. Só o temos aqui para poder consultar o manipulador de interrupções. Dado um endereço de currículo, podemos simplesmente diminuir o ponteiro em 8 e temos o endereço dessa interrupção de currículo. O "mov r15" no nosso caso serve apenas para nos permitir desmontar o código corretamente; poderíamos simplesmente incorporar o endereço sozinho, mas isso confundiria qualquer desmontador externo.

O problema real

Agora, na nova versão, não há "prepareMethodYielding", mas apenas uma chamada - pelo menos, de forma ideal. Mas a "ligação" por si só não nos permite fazer um endereço de retorno modificado, então aqui me deparo com algumas opções e quero saber qual é a melhor.

Opção A - lea + push + jmp

Nossa primeira opção é simular a "chamada", mas enviar o endereço de retorno manualmente:

lea         rax,[rip+10h]
push        rax
jmp         A6 // yieldingMethod

Isto requer 3 instruções, mas nenhum acesso à memória.

Opção B - enviar da memória

Poderíamos reduzir o número de opções armazenando o endereço de retorno em alguma área da memória constante:

push        qword ptr[rip+1234] // return-address stored here
jmp         A6                  // yieldingMethod

Agora precisamos apenas de um push e nenhum registro intermediário, embora agora precisemos de acesso à memória, que poderia estar potencialmente mais distante na seção de dados.

Opção C – modificar o endereço de retorno na função chamada

Outra opção que vejo seria ajustar o endereço de retorno produzido pela chamada dentro do método chamado. Todos esses métodos aqui são compilados usando minha própria convenção de chamada, portanto, não aderem ao x64 ou a qualquer outro.

// caller
call     A6                 // yielding method

// callee, first instruction
add      qword ptr[rsp],10  // size of interrupt-embedding is always the same

Também seria apenas uma instrução, com uma pequena codificação. Embora apenas do ponto de vista do design, eu não goste muito disso, pois acopla as informações sobre a incorporação do receptor ao chamador - embora, se essa fosse a variante mais eficiente, eu ainda poderia optar por ela.

Opção D - não modifique o endereço de retorno

Nossa última opção é não modificar o endereço de retorno, mas sim alterar a forma como a pesquisa e o retorno são tratados.

call      12345;                // yieldingMethod
mov       r15,interruptAddress; // is actually executed now (but value is not used)

Então aqui, mudaríamos onde procuramos o endereço de interrupção (já que o endereço de retorno agora aponta na frente da instrução falsa, em vez de atrás dela). Então, ao retornar da chamada, executaríamos a instrução movebs, mas descartaríamos o valor carregado. A vantagem aqui é que o tamanho geral do código é o menor, já que não precisamos adicionar instruções adicionais que ainda não estejam lá. No entanto, estamos executando uma instrução mov de 10 bytes, que pode ser mais lenta do que algumas das outras variantes. Depende aqui do que a CPU está fazendo - se ela já decodifica a instrução falsa, mesmo que não a alcance diretamente, pode ser a melhor ideia apenas executá-la, em vez de modificar o endereço de retorno. A mesma coisa, se a CPU puder de alguma forma detectar que a instrução não tem efeito, já que seu valor nunca é lido, durante a renomeação de registros, então ele poderia ser efetivamente gratuito - atm, estou usando um registro que não é usado, para distinguir meu próprio montador; mas provavelmente faria sentido usar um registro que fosse sobrescrito logo depois, presumo. Embora eu não tenha certeza do que realmente aconteceria aqui.

Conclusão

Então, qual dessas 4 opções lhe parece mais eficiente? Também estou aberto a outras idéias, embora o design geral de como as corrotinas são feitas seja finalizado e funcional, então algo como usar uma abordagem baseada em máquina de estados que algumas corrotinas do IIRC usam não é realmente uma opção aqui.

assembly
  • 1 respostas
  • 60 Views
Martin Hope
Juliean
Asked: 2024-01-08 19:12:32 +0800 CST

Suposições sobre dwPageSize em diferentes sistemas

  • 8

Podemos fazer alguma suposição sobre SYSTEM_INFO dwPageSize em sistemas diferentes, ao direcionar a mesma arquitetura (ou seja, x86_64)?

Eu gero algum código nativo personalizado, que é carregado junto com um aplicativo C++ via VirtualAlloc. Este código possui 3 seções diferentes (código, cdata, variáveis ​​estáticas dinâmicas), que necessitam de proteções diferentes (execução; leitura; leitura-gravação) via VirtualProtect e, portanto, precisam estar em páginas diferentes. O código faz referência a cdata e variáveis ​​estáticas por meio de endereçamento relativo ao RIP.

Estou me perguntando se posso assumir que, digamos, ao construir em x64, dwPageSize é 4096, ele também terá o mesmo valor em outros sistemas x64 (ou pelo menos menores, mas nunca maiores)? Nesse caso, posso simplesmente considerar os endereços relativos ao RIP como estão, porque posso garantir que todos os dados sejam colocados nos mesmos deslocamentos relativos do tamanho da página. Se as páginas no sistema de destino pudessem ser potencialmente maiores, eu precisaria consertar essas compensações ao carregar o código.

c++
  • 1 respostas
  • 78 Views

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Reformatar números, inserindo separadores em posições fixas

    • 6 respostas
  • Marko Smith

    Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não?

    • 2 respostas
  • Marko Smith

    Problema com extensão desinstalada automaticamente do VScode (tema Material)

    • 2 respostas
  • Marko Smith

    Vue 3: Erro na criação "Identificador esperado, mas encontrado 'import'" [duplicado]

    • 1 respostas
  • Marko Smith

    Qual é o propósito de `enum class` com um tipo subjacente especificado, mas sem enumeradores?

    • 1 respostas
  • Marko Smith

    Como faço para corrigir um erro MODULE_NOT_FOUND para um módulo que não importei manualmente?

    • 6 respostas
  • Marko Smith

    `(expression, lvalue) = rvalue` é uma atribuição válida em C ou C++? Por que alguns compiladores aceitam/rejeitam isso?

    • 3 respostas
  • Marko Smith

    Um programa vazio que não faz nada em C++ precisa de um heap de 204 KB, mas não em C

    • 1 respostas
  • Marko Smith

    PowerBI atualmente quebrado com BigQuery: problema de driver Simba com atualização do Windows

    • 2 respostas
  • Marko Smith

    AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos

    • 1 respostas
  • Martin Hope
    Fantastic Mr Fox Somente o tipo copiável não é aceito na implementação std::vector do MSVC 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant Encontre o próximo dia da semana usando o cronógrafo 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor O inicializador de membro do construtor pode incluir a inicialização de outro membro? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul O C++20 mudou para permitir a conversão de `type(&)[N]` de matriz de limites conhecidos para `type(&)[]` de matriz de limites desconhecidos? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann Como/por que {2,3,10} e {x,3,10} com x=2 são ordenados de forma diferente? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller O ponto e vírgula agora é opcional em condicionais bash com [[ .. ]] na versão 5.2? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench Por que um traço duplo (--) faz com que esta cláusula MariaDB seja avaliada como verdadeira? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng Por que `dict(id=1, **{'id': 2})` às vezes gera `KeyError: 'id'` em vez de um TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos 2024-03-20 03:12:31 +0800 CST

Hot tag

python javascript c++ c# java typescript sql reactjs html

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve