É seguro ler além do final de um buffer na mesma página em x86 e x64?

Muitos métodos encontrados em algoritmos de alto desempenho podem ser (e são) simplificados se lhes for permitido ler umPequena quantidade após o final dos buffers de entrada. Aqui, "pequena quantidade" geralmente significa atéW - 1 bytes após o final, ondeW é o tamanho da palavra em bytes do algoritmo (por exemplo, até 7 bytes para um algoritmo que processa a entrada em blocos de 64 bits).

Está claro queescrita$7Pequena quantidade8$

No caso especial da leitura de valores alinhados, no entanto, uma falha de página parece impossível, pelo menos no x86. Nessa plataforma, as páginas (e, portanto, os sinalizadores de proteção de memória) têm uma granularidade de 4K (são possíveis páginas maiores, por exemplo, 2MiB ou 1GiB, mas são múltiplos de 4K) e, portanto, as leituras alinhadas acessam apenas bytes na mesma página que a válida parte do buffer.

Aqui está um exemplo canônico de algum loop que alinha sua entrada e lê até 7 bytes após o final do buffer:

int processBytes(uint8_t *input, size_t size) {

    uint64_t *input64 = (uint64_t *)input, end64 = (uint64_t *)(input + size);
    int res;

    if (size < 8) {
        // special case for short inputs that we aren't concerned with here
        return shortMethod();
    }

    // check the first 8 bytes
    if ((res = match(*input)) >= 0) {
        return input + res;
    }

    // align pointer to the next 8-byte boundary
    input64 = (ptrdiff_t)(input64 + 1) & ~0x7;

    for (; input64 < end64; input64++) {
        if ((res = match(*input64)) > 0) {
            return input + res < input + size ? input + res : -1;
        }
    }

    return -1;
}

A função internaint match(uint64_t bytes) não é mostrado, mas é algo que procura um byte que corresponda a um determinado padrão e retorna a posição mais baixa (0-7) se encontrada ou -1 caso contrário.

Primeiro, casos com tamanho <8 são penhorados para outra função para simplificar a exposição. Em seguida, é feita uma única verificação para os 8 primeiros (bytes não alinhados). Em seguida, um loop é feito para o restantefloor((size - 7) / 8)$14 após o final dos buffers de entrada. Aqui, "pequena quantidade" geralmente significa até15$input & 0xF == 1) No entanto, a chamada de retorno tem um cheque que exclui qualquercorrespondências falsas que ocorrem além do final do buffer.

Na prática, essa função é segura em x86 e x86-64?

Esses tipos deoverreads são comuns em código de alto desempenho. Código de cauda especial para evitar taisoverreads também é comum. Às vezes, você vê o último tipo substituindo o primeiro para silenciar ferramentas como o valgrind. Às vezes você vê umproposta para fazer essa substituição, que é rejeitada com base no fato de o idioma ser seguro e a ferramenta estar errada (ou simplesmente muito conservadora)3.

Uma observação para os advogados de idiomas:

Definitivamente, a leitura de um ponteiro além do tamanho alocado não é permitida no padrão. Aprecio as respostas dos advogados de idiomas e até mesmo as escrevo ocasionalmente, e ficarei feliz quando alguém desenterrar o capítulo e o verso que mostra o código acima.comportamento indefinido e, portanto, não é seguro no sentido mais estrito (e vou copiar os detalhes aqui). Em última análise, porém, não é isso que eu estou procurando. Por uma questão prática, muitos idiomas comuns que envolvem a conversão de ponteiros, o acesso à estrutura por esses ponteiros e por isso são tecnicamente indefinidos, mas são difundidos em código de alta qualidade e alto desempenho. Freqüentemente não há alternativa, ou a alternativa funciona a meia velocidade ou menos.

Se desejar, considere uma versão modificada desta pergunta, que é:

Depois que o código acima foi compilado no assembly x86 / x86-64 e o usuário verificou que ele é compilado da maneira esperada (ou seja, o compilador não usou um acesso parcialmente fora dos limites provável para fazer algorealmente inteligente, é seguro executar o programa compilado?

A esse respeito, essa pergunta é uma pergunta C e uma questão de montagem x86. A maior parte do código usando esse truque que eu vi é escrita em C, e C ainda é a linguagem dominante para bibliotecas de alto desempenho, eclipsando facilmente coisas de nível inferior como asm e coisas de nível superior como <tudo o resto>. Pelo menos fora do nicho numérico em que FORTRAN ainda joga bola. Então, eu estou interessado noCompilador C e abaixo visão da questão, e é por isso que não a formulei como uma questão de montagem x86 pura.

Tudo isso dito, embora eu esteja apenas moderadamente interessado em um link para o padrão que mostre isso é UD, estou muito interessado em todos os detalhes de implementações reais que podem usar esse UD específico para produzir código inesperado. Agora eu nãopensar isso pode acontecer sem uma análise profunda entre procedimentos, mas o excesso de gcc surpreendeu muita gente ...

1 Mesmo em casos aparentemente inofensivos, por exemplo, onde o mesmo valor é gravado de volta, ele podequebrar código simultâneo.

2 Nota para que essa sobreposição funcione requer que esta função ematch() função para se comportar de uma maneira idempotente específica - em particular que o valor de retorno suporta verificações sobrepostas. Portanto, um "encontrar padrão de correspondência de primeiro byte" funciona, poismatch() as chamadas ainda estão em ordem. Um método "contar bytes que correspondam ao padrão" não funcionaria, pois alguns bytes poderiam ser contados duas vezes. Como um aparte: algumas funções como a chamada "retornar o byte mínimo" funcionariam mesmo sem a restrição em ordem, mas precisam examinar todos os bytes.

3 Vale a pena notar aqui que, para o Memcheck de valgrindhá uma bandeira, --partial-loads-ok que controla se essas leituras são de fato relatadas como um erro. O padrão ésim, significa que, em geral, essas cargas não são tratadas como erros imediatos, mas que é feito um esforço para rastrear o uso subsequente de bytes carregados, alguns dos quais são válidos e outros não, com um erro sendo sinalizado se o bytes de intervalo sãousava. Em casos como o exemplo acima, em que a palavra inteira é acessada emmatch(), essa análise concluirá que os bytes são acessados, mesmo que os resultados sejam descartados. Valgrindnão pode em geral determinar se bytes inválidos de uma carga parcial são realmente usados (e a detecção em geral é provavelmentemuito Difícil).

questionAnswers(2)

yourAnswerToTheQuestion