As funções de uma biblioteca C devem sempre esperar o comprimento de uma string?

14

Atualmente, estou trabalhando em uma biblioteca escrita em C. Muitas funções dessa biblioteca esperam uma string como char* ou const char* em seus argumentos. Comecei com essas funções sempre esperando o comprimento da string como size_t , de modo que a terminação nula não era necessária. No entanto, ao escrever testes, isso resultou no uso frequente de strlen() , assim:

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

Confiar no usuário para passar as strings corretamente terminadas levaria a um código menos seguro, porém mais conciso e (na minha opinião) legível:

libFunction("I hope there's a null-terminator there!");

Então, qual é a prática sensata aqui? Tornar a API mais complicada de usar, mas forçar o usuário a pensar em sua entrada ou documentar o requisito para uma cadeia terminada com nulo e confiar no chamador?

    
por Benjamin Kloster 12.06.2012 / 20:42
fonte

7 respostas

4

Definitivamente, e absolutamente, carregam o comprimento . A biblioteca C padrão é infamemente quebrada desta forma, o que não causou nenhum fim de dor ao lidar com estouro de buffer. Essa abordagem é o foco de tanto ódio e angústia que os compiladores modernos vão realmente alertar, lamentar e reclamar quando usarem essas funções de biblioteca padrão.

É tão ruim, que se você se deparar com essa pergunta em uma entrevista - e seu entrevistador técnico parece que ele tem alguns anos de experiência - puro fanático pode conseguir o emprego - você pode realmente ficar muito à frente se você pode citar o precedente de fotografar alguém implementando APIs procurando pelo terminador de cadeia C.

Deixando a emoção de lado, há muita coisa que pode dar errado com o NULL no final da string, tanto na leitura quanto na manipulação - além disso, isso está realmente em violação direta dos conceitos de design moderno, como defesa. em profundidade (não necessariamente aplicado à segurança, mas ao design da API). Exemplos de APIs C que carregam o comprimento são abundantes - ex. a API do Windows.

Na verdade, este problema foi resolvido em algum momento nos anos 90, o consenso emergente de hoje é que você não deveria nem tocar nas suas strings .

Later edit : este é um debate ao vivo, então vou adicionar que confiar em todos abaixo e acima de você para ser legal e usar as funções str * da biblioteca é OK, até ver coisas clássicas como output = malloc(strlen(input)); strcpy(output, input); ou while(*src) { *dest=transform(*src); dest++; src++; } . Eu quase posso ouvir Lacrimosa de Mozart no fundo.

    
por 12.06.2012 / 21:06
fonte
17

Em C, a expressão idiomática é que as cadeias de caracteres são terminadas em NUL, portanto, faz sentido seguir a prática comum - é relativamente improvável que os usuários da biblioteca tenham sequências não terminadas em NUL (já que precisam de extra trabalho para imprimir usando printf e usar em outro contexto). Usar qualquer outro tipo de string não é natural e provavelmente é relativamente raro.

Além disso, sob as circunstâncias, seu teste parece um pouco estranho para mim, já que para funcionar corretamente (usando strlen), você está assumindo uma string terminada em NUL em primeiro lugar. Você deve estar testando o caso de sequências não terminadas em NUL se você pretende que sua biblioteca trabalhe com elas.

    
por 12.06.2012 / 20:53
fonte
10

Seu argumento de "segurança" não é válido. Se você não confia no usuário para entregar a você uma string terminada em null quando é isso que você documentou (e o que é "a norma" para C simples), você não pode realmente confiar no tamanho que eles lhe dão (o que eles Provavelmente, use strlen da mesma forma que você está fazendo, se eles não estiverem à mão, e quais falharão se a "string" não for uma string, em primeiro lugar.

Existem razões válidas para requerer um comprimento: se você quer que suas funções trabalhem em substrings, é bem mais fácil (e eficiente) passar um tamanho do que fazer o usuário fazer alguma mágica de cópia para trás e para frente. o byte nulo no lugar certo (e corre o risco de erros ao longo do caminho).
Ser capaz de manipular codificações em que bytes nulos não são terminações ou poder manipular cadeias que tenham nulos incorporados (propositalmente) pode ser útil em algumas circunstâncias (depende do que suas funções normalmente fazem). Ser capaz de lidar com dados não terminados em nulo (matrizes de comprimento fixo) também é útil. Resumindo: depende do que você está fazendo em sua biblioteca e do tipo de dados que espera que seus usuários estejam manipulando.

Há também possivelmente um aspecto de desempenho para isso. Se sua função precisa saber antecipadamente o tamanho da string, e você espera que seus usuários, pelo menos, usualmente já conheçam essa informação, tê-los repassando (ao invés de calculá-la) poderia reduzir alguns ciclos.

Mas se sua biblioteca espera cadeias de texto ASCII simples e comuns, e você não tem restrições de desempenho insuportáveis e uma boa compreensão de como seus usuários irão interagir com sua biblioteca, adicionar um parâmetro de tamanho não parece uma boa ideia . Se a string não for terminada corretamente, provavelmente o parâmetro length será tão falso. Eu não acho que você vai ganhar muito com isso.

    
por 12.06.2012 / 21:06
fonte
2

Não. Strings são sempre terminadas por caractere nulo por definição, o tamanho da string é redundante.

Dados de caractere não terminados em nulo nunca devem ser chamados de "string". Processá-lo (e lançar comprimentos) deve ser normalmente encapsulado em uma biblioteca, e não parte da API. Exigir o comprimento como um parâmetro apenas para evitar chamadas únicas de strlen () é provável Premature Optimization.

Confiar no chamador de uma função da API não é inseguro ; O comportamento indefinido é perfeitamente aceitável se as pré-condições documentadas não forem cumpridas.

Naturalmente, uma API bem projetada não deve conter armadilhas e deve facilitar o uso correto. E isso significa apenas que deve ser o mais simples e direto possível, evitando redundâncias e seguindo as convenções da linguagem.

    
por 09.05.2015 / 00:55
fonte
1

Você deve sempre manter seu comprimento ao redor. Por um lado, seus usuários podem querer conter NULLs neles. E, em segundo lugar, não esqueça que strlen é O (N) e requer tocar todo o cache de strings bye bye. E, em terceiro lugar, facilita a transmissão de subconjuntos - por exemplo, eles poderiam dar menos do que o tamanho real.

    
por 12.06.2012 / 21:11
fonte
1

Você deve distinguir entre passar uma string e passar por um buffer .

Em C, as cadeias são tradicionalmente terminadas em NUL. É perfeitamente razoável esperar isso. Portanto, geralmente não há necessidade de passar o comprimento da corda; pode ser calculado com strlen , se necessário.

Ao passar em torno de um buffer , especialmente um que é escrito para, então você deve absolutamente passar o tamanho do buffer. Para um buffer de destino, isso permite que o receptor certifique-se de que ele não estourará o buffer. Para um buffer de entrada, ele permite que o receptor evite ler após o final, especialmente se o buffer de entrada contiver dados arbitrários originados de uma fonte não confiável.

Há talvez alguma confusão porque as cadeias de caracteres e os buffers podem ser char* e porque muitas funções de cadeia geram novas cadeias gravando nos buffers de destino. Algumas pessoas concluem que as funções de string devem ter comprimentos de string. No entanto, esta é uma conclusão imprecisa. A prática de incluir um tamanho com um buffer (seja esse buffer usado para strings, arrays de inteiros, estruturas, qualquer que seja) é um mantra mais útil e mais geral.

(No caso de ler uma string de uma fonte não confiável (por exemplo, um soquete de rede), é importante fornecer um tamanho, pois a entrada pode não ser terminada em NUL. No entanto , você deve não considere a entrada como uma string.Você deve tratá-la como um buffer de dados arbitrário que pode conter uma string (mas você não sabe até validá-la) , portanto, isso ainda segue o princípio de que os buffers devem ter tamanhos associados e que as strings não precisam deles.

    
por 22.03.2017 / 11:40
fonte
0

Se as funções são usadas principalmente com literais de string, a dificuldade de lidar com comprimentos explícitos pode ser minimizada definindo algumas macros. Por exemplo, dada uma função da API:

void use_string(char *string, int length);

pode-se definir uma macro:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

e, em seguida, invoque-o como mostrado em:

void test(void)
{
  use_strlit("Hello");
}

Embora seja possível criar coisas "criativas" para passar macro que irá compilar mas não irá funcionar, o uso de "" em qualquer lado da string dentro da avaliação de "sizeof" deve pegar tentativas acidentais de usar ponteiros de caractere diferentes de literais de string decompostos [na ausência desses "" , uma tentativa de passar um ponteiro de caractere erroneamente forneceria o comprimento como o tamanho de um ponteiro, menos um.

Uma abordagem alternativa no C99 seria definir um tipo de estrutura "pointer and length" e definir uma macro que converta uma string literal em um literal composto desse tipo de estrutura. Por exemplo:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

Note que se alguém usa tal abordagem, deve-se passar tais estruturas por valor ao invés de passar por seus endereços. Caso contrário, algo como:

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

pode falhar, uma vez que o tempo de vida dos literais compostos terminaria nos extremos das declarações anexas.

    
por 21.03.2017 / 22:50
fonte

Tags