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

15

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 um size_tpara que a terminação nula não fosse 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 transmitir seqüências finalizadas corretamente levaria a um código menos seguro, mas 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? Torne a API mais complicada de usar, mas force o usuário a pensar em sua entrada ou a documentar o requisito para uma sequência terminada em nulo e confiar no chamador?

Benjamin Kloster
fonte

Respostas:

4

Definitivamente e absolutamente carregam o comprimento . A biblioteca C padrão é infamemente quebrada dessa maneira, o que não causou nenhum problema ao lidar com estouros de buffer. Essa abordagem é o foco de tanto ódio e angústia que os compiladores modernos realmente alertam, reclamam e reclamam ao usar esse tipo de funções padrão da biblioteca.

É tão ruim que, se você se deparar com essa pergunta em uma entrevista - e seu entrevistador técnico parecer ter alguns anos de experiência - o puro fanatismo pode conseguir o emprego - você poderá chegar muito à frente se puder citar o precedente de fotografar alguém implementando APIs procurando o terminador de cadeia C.

Deixando de lado a emoção de tudo isso, há muita coisa que pode dar errado com esse NULL no final da sua string, tanto na leitura quanto na manipulação - além disso, ele realmente viola diretamente os 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.

De fato, esse problema foi resolvido em algum momento dos anos 90, o consenso emergente de hoje é que você nem deve tocar suas cordas .

Edição posterior : este é um debate bastante ativo, portanto, acrescentarei que confiar em todos que estão abaixo e acima de você para ser legal e usar as funções da biblioteca str * está OK, até que você veja coisas clássicas como output = malloc(strlen(input)); strcpy(output, input);ou while(*src) { *dest=transform(*src); dest++; src++; }. Quase consigo ouvir o Lacrimosa de Mozart ao fundo.

vski
fonte
1
Não entendo seu exemplo da API do Windows que exige que o chamador forneça o comprimento das strings. Por exemplo, uma função típica da API do Win32, como CreateFilerecebe um LPTCSTR lpFileNameparâmetro como entrada. Nenhum comprimento da sequência é esperado do chamador. De fato, o uso de cadeias terminadas em NUL é tão arraigado que a documentação nem menciona que o nome do arquivo deve ser terminado por NUL (mas é claro que deve ser).
Greg Hewgill
1
Na verdade, no Win32, o LPSTRtipo diz que as seqüências de caracteres podem ter terminação NUL e, se não , isso será indicado na especificação associada. Portanto, a menos que seja indicado de outra forma, espera-se que essas seqüências de caracteres no Win32 sejam encerradas por NUL.
Greg Hewgill
Ótimo ponto, eu era impreciso. Considere que o CreateFile e seu grupo existem desde o Windows NT 3.1 (início dos anos 90); a API atual (ou seja, desde a introdução do Strsafe.h no XP SP2 - com desculpas públicas da Microsoft) reprovou explicitamente todas as coisas terminadas em NULL que podia. A primeira vez que a Microsoft realmente sentiu muito pelo uso de seqüências terminadas em NULL foi na verdade muito mais cedo, quando eles tiveram que introduzir o BSTR na especificação OLE 2.0, para trazer de alguma forma o VB, COM e o antigo WINAPI no mesmo barco.
vski 12/06
1
Mesmo em, StringCbCatpor exemplo, apenas o destino tem um buffer máximo, o que faz sentido. A fonte ainda é uma sequência C comum com terminação NUL. Talvez você possa melhorar sua resposta esclarecendo a diferença entre um parâmetro de entrada e um parâmetro de saída . Os parâmetros de saída devem sempre ter um tamanho máximo de buffer; os parâmetros de entrada geralmente são terminados em NUL (existem exceções, mas raras na minha experiência).
Greg Hewgill
1
Sim. As strings são imutáveis ​​na JVM / Dalvik e no .NET CLR no nível da plataforma, bem como em muitos outros idiomas. Eu iria tão longe e especularia que o mundo nativo ainda não consegue fazer isso (o padrão C ++ 11) por causa de a) legado (você realmente não ganha muito por ter apenas parte de suas seqüências imutáveis) eb ) você realmente precisa de um GC e uma tabela de strings para fazer isso funcionar, os alocadores com escopo definido no C ++ 11 não conseguem cortá-lo.
vski 12/06
16

Em C, o idioma é que as cadeias de caracteres são terminadas com NUL, por isso faz sentido respeitar a prática comum - é relativamente improvável que os usuários da biblioteca tenham cadeias sem terminação NUL (uma vez que precisam de trabalho extra para imprimir usando printf e use em outro contexto). O uso de qualquer outro tipo de corda não é natural e provavelmente relativamente raro.

Além disso, nessas circunstâncias, seu teste me parece um pouco estranho, pois, para funcionar corretamente (usando strlen), você está assumindo uma string terminada por NUL em primeiro lugar. Você deve testar o caso de cadeias não terminadas por NUL se pretende que sua biblioteca trabalhe com elas.

James McLeod
fonte
-1, desculpe, isso é simplesmente desaconselhável.
vski 12/06
Antigamente, isso nem sempre era verdade. Eu trabalhei muito com protocolos binários que colocavam dados de string em campos de comprimento fixo que não eram terminados em NULL. Nesses casos, era muito trabalhoso trabalhar com funções que demoravam muito. Eu não faço C há uma década, no entanto.
Gort the Robot
4
@vski, como forçar o usuário a chamar 'strlen' antes de chamar a função target faz qualquer coisa para evitar problemas de estouro de buffer? Pelo menos, se você verificar o comprimento dentro da função de destino, poderá ter certeza de qual sentido de comprimento está sendo usado (incluindo o terminal nulo ou não).
Charles E. Grant
@ Charles E. Grant: Veja o comentário acima sobre StringCbCat e StringCbCatN em Strsafe.h. Se você apenas possui um caractere * e não possui comprimento, então, na verdade, você não tem escolha real a não ser usar as funções str *, mas o objetivo é carregar o comprimento ao redor, tornando-se uma opção entre str * e strn * funções cujas últimas são preferidas.
vski 13/06
2
@vski Não há necessidade de passar o comprimento de uma string . Não é necessário para passar em torno de um tampão de comprimento 's. Nem todas as buffers são cadeias, e nem todas as cadeias são buffers.
Jamesdlin # 22/17
10

Seu argumento de "segurança" realmente não se sustenta. Se você não confia no usuário para entregar a você uma string terminada em nulo quando é isso que você documentou (e qual é "a norma" para C simples), você não pode realmente confiar no tamanho que ele fornece (o que eles provavelmente strlenusará exatamente como você está fazendo, se não o tiverem à mão e que falhará se a "string" não fosse uma string em primeiro lugar).

Porém, existem razões válidas para exigir um comprimento: se você deseja que suas funções funcionem em substrings, é possivelmente muito mais fácil (e eficiente) passar um comprimento do que fazer com que o usuário faça alguma mágica de cópia para obter o byte nulo no lugar certo (e corra o risco de erros pontuais ao longo do caminho).
Ser capaz de lidar com codificações em que os bytes nulos não são terminações, ou ser capaz de lidar com seqüências que possuem nulos incorporados (de propósito) podem ser úteis em algumas circunstâncias (depende do que exatamente suas funções fazem).
Ser capaz de lidar com dados com terminação não nula (matrizes de comprimento fixo) também é útil.
Em resumo: depende do que você está fazendo na sua biblioteca e de que tipo de dados você espera que seus usuários estejam lidando.

Também há possivelmente um aspecto de desempenho nisso. Se sua função precisar conhecer o comprimento da string com antecedência e você esperar que seus usuários, pelo menos em geral, já conheçam essas informações, fazer com que elas as passem (em vez de calculá-las) pode reduzir alguns ciclos.

Mas se sua biblioteca espera seqüências de texto ASCII comuns comuns e você não tem restrições de desempenho insuportáveis ​​e um entendimento muito bom de como seus usuários irão interagir com sua biblioteca, adicionar um parâmetro de comprimento não parece uma boa idéia. Se a string não for finalizada corretamente, é provável que o parâmetro length seja igualmente falso. Eu não acho que você vai ganhar muito com isso.

Esteira
fonte
Discordo totalmente desta abordagem. Nunca confie em seus chamadores, especialmente por trás de uma API de biblioteca, faça o possível para questionar as coisas que eles oferecem e falhe normalmente. Carregue o comprimento desejado, trabalhar com seqüências terminadas em NULL não é o que "se soltar com seus chamadores e estrito com seus callees" significa.
vski 12/06
2
Concordo principalmente com a sua posição, mas você parece depositar muita confiança nesse argumento de comprimento - não há razão para que seja confiável que o terminador nulo. Minha posição é que depende do que a biblioteca faz.
Mat
Há muito mais que pode dar errado com o terminador NULL em seqüências de caracteres do que com o comprimento passado por valor. Em C, a única razão pela qual alguém confiaria no comprimento é porque seria irracional e impraticável não carregar o tamanho do buffer não é uma boa resposta, é apenas o melhor considerando as alternativas. Essa é uma das razões pelas quais seqüências de caracteres (e buffers em geral) são ordenadamente compactadas e encapsuladas em idiomas RAD.
vski 12/06
2

Não. As strings são sempre terminadas em nulo por definição, o comprimento da string é redundante.

Os dados de caracteres com terminação não nula nunca devem ser chamados de "string". Processá-lo (e dar voltas) geralmente deve ser encapsulado em uma biblioteca e não parte da API. É provável que exigir o comprimento como parâmetro apenas para evitar chamadas strlen () únicas. Otimização prematura.

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

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

dpi
fonte
não apenas perfeitamente ok, mas inevitável, a menos que se mude para um idioma de thread único e com segurança de memória. Poderia ter deixado cair algumas restrições mais necessário marcar ...
Deduplicator
1

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

DeadMG
fonte
4
Se a função de biblioteca lida com NULLs incorporados em cadeias precisa ser muito bem documentado. A maioria das funções da biblioteca C para em NULL ou comprimento, o que ocorrer primeiro. (E se por escrito com competência, aquelas que não tomam comprimento nunca use strlenem um teste de loop.)
Gort o robô
1

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

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

Ao passar em torno de um buffer , especialmente um que é gravado, você deve absolutamente passar o tamanho do buffer. Para um buffer de destino, isso permite que o receptor garanta que ele não exceda o buffer. Para um buffer de entrada, ele permite que o receptor evite ler além do final, especialmente se o buffer de entrada contiver dados arbitrários originários de uma fonte não confiável.

Talvez haja alguma confusão, porque as strings e os buffers poderiam ser char*e porque muitas funções de string geram novas strings 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 em um buffer (seja esse buffer usado para strings, matrizes de números 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 comprimento, pois a entrada pode não ter a terminação NUL. No entanto , você não deve considerar a entrada como uma string. deve tratá-lo como um buffer de dados arbitrário que pode conter uma string (mas você não sabe até que você realmente a valide), para que isso ainda siga o princípio de que os buffers devem ter tamanhos associados e que as strings não precisam deles.

jamesdlin
fonte
É exatamente isso que a pergunta e outras respostas perderam.
Blrfl
0

Se as funções forem usadas principalmente com literais de seqüência de caracteres, a dificuldade de lidar com comprimentos explícitos pode ser minimizada pela definição de algumas macros. Por exemplo, dada uma função de API:

void use_string(char *string, int length);

pode-se definir uma macro:

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

e invoque-o como mostrado em:

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

Embora seja possível criar coisas "criativas" para passar a macro que será compilada, mas que na verdade não funcionará, o uso de ""ambos os lados da string na avaliação de "sizeof" deve capturar tentativas acidentais de usar caracteres. ponteiros que não sejam literais de string decompostos [na ausência deles "", uma tentativa de passar um ponteiro de caractere daria erroneamente o comprimento do tamanho de um ponteiro, menos um.

Uma abordagem alternativa no C99 seria definir um tipo de estrutura "ponteiro e comprimento" e definir uma macro que converta uma literal de cadeia de caracteres 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})

Observe que, se alguém usa essa abordagem, deve passar essas estruturas por valor, em vez de passar 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, pois a vida útil dos literais compostos terminaria no final de suas instruções anexas.

supercat
fonte