Es comprensible que pasar por alto un búfer falle (o crea un desbordamiento), pero ¿qué sucede si se usan menos de 12 bytes en un búfer de 12 bytes? ¿Es posible o el final vacío siempre se llena con 0s? Pregunta ortogonal que puede ayudar: ¿qué contiene un búfer cuando se instancia pero aún no lo usa la aplicación?

He visto algunos programas favoritos en Visual Studio y parece que se agregan con 0 (o caracteres nulos), pero no estoy seguro de si esta es una implementación de MS que puede variar según el idioma / compilador.

28
hexadec0079 17 sep. 2018 a las 04:59

10 respuestas

La mejor respuesta

Considere su búfer, lleno de ceros:

[00][00][00][00][00][00][00][00][00][00][00][00]

Ahora, le escribiremos 10 bytes. Valores que se incrementan desde 1:

[01][02][03][04][05][06][07][08][09][10][00][00]

Y ahora nuevamente, esta vez, 4 veces 0xFF:

[FF][FF][FF][FF][05][06][07][08][09][10][00][00]

¿Qué sucede si hay menos de 12 bytes utilizados en un búfer de 12 bytes? ¿Es posible o el final vacío siempre se llena con 0s?

Escribe tanto como desee, los bytes restantes no se modifican.

Pregunta ortogonal que puede ayudar: ¿qué contiene un búfer cuando se instancia pero aún no lo usa la aplicación?

Sin especificar Espere basura dejada por los programas (u otras partes de su programa) que usaron esta memoria antes.

He visto algunos programas favoritos en Visual Studio y parece que se agregan con 0 (o caracteres nulos), pero no estoy seguro de si esta es una implementación de MS que puede variar según el idioma / compilador.

Es exactamente lo que crees que es. Esta vez alguien lo había hecho por usted, pero no hay garantías de que vuelva a suceder. Podría ser un indicador del compilador que adjunte código de limpieza. Algunas versiones de MSVC solían llenar memoria nueva con 0xCD cuando se ejecutaba en depuración pero no en versión. También puede ser una característica de seguridad del sistema que borra la memoria antes de entregarla a su proceso (para que no pueda espiar otras aplicaciones). Recuerde siempre usar memset para inicializar su búfer donde sea importante. Eventualmente, ordene usar cierto indicador del compilador en el archivo Léame si depende de un búfer nuevo para contener un cierto valor.

Pero la limpieza no es realmente necesaria. Toma un búfer de 12 bytes de longitud. Lo llenas con 7 bytes. Luego lo pasa a alguna parte y dice "aquí hay 7 bytes para usted". El tamaño del búfer no es relevante al leerlo. Espera que otras funciones lean tanto como ha escrito, no tanto como sea posible. De hecho, en C generalmente no es posible saber cuánto dura el búfer.

Y una nota al margen:

Es comprensible que al revisar un búfer se produzcan errores (o crea un desbordamiento)

No es así, ese es el problema. Es por eso que es un gran problema de seguridad: no hay ningún error y el programa intenta continuar, por lo que a veces ejecuta el contenido malicioso que nunca pretendió. Así que tuvimos que agregar un montón de mecanismos al sistema operativo, como ASLR, que aumentará la probabilidad de que el programa se bloquee y disminuya la probabilidad de que continúe con la memoria dañada. Por lo tanto, nunca dependa de esos guardias de último momento y observe sus límites de amortiguación usted mismo.

11
Agent_L 17 sep. 2018 a las 13:26

Creo que la respuesta correcta es que siempre debe realizar un seguimiento de cuántos caracteres se escriben. Al igual que con las funciones de bajo nivel, como leer y escribir, necesita o da el número de caracteres leídos o escritos. De la misma manera, std :: string realiza un seguimiento del número de caracteres en su implementación

1
izulh 17 sep. 2018 a las 21:09

Escribir parte de un búfer no afectará la parte no escrita del búfer; contendrá lo que estaba allí de antemano (que, naturalmente, depende completamente de cómo obtuvo el búfer en primer lugar).

Como señala la otra respuesta, las variables estáticas y globales se inicializarán a 0, pero las variables locales no se inicializarán (y en su lugar contendrán lo que estaba en la pila de antemano). Esto está en consonancia con el principio de sobrecarga cero: inicializar variables locales sería, en algunos casos, un costo de tiempo de ejecución innecesario y no deseado, mientras que las variables estáticas y globales se asignan en tiempo de carga como parte de un segmento de datos.

La inicialización del almacenamiento dinámico es una opción del administrador de memoria, pero en general tampoco se inicializará.

2
comingstorm 17 sep. 2018 a las 02:14

Tome el siguiente ejemplo (dentro de un bloque de código, no global):

char data[12];
memcpy(data, "Selbie", 6);

O incluso este ejemplo:

char* data = new char[12];
memcpy(data, "Selbie", 6);

En los dos casos anteriores, los primeros 6 bytes de data son S, e, l, b, i y { {X6}}. Los 6 bytes restantes de data se consideran "no especificados" (podría ser cualquier cosa).

¿Es posible o el final vacío siempre se llena con 0s?

No garantizado en absoluto. El único asignador que conozco que garantiza el llenado de cero bytes es calloc. Ejemplo:

char* data = calloc(12,1);  // will allocate an array of 12 bytes and zero-init each byte
memcpy(data, "Selbie");

¿Qué contiene un búfer cuando se crea una instancia pero aún no lo usa la aplicación?

Técnicamente, según los estándares C ++ más recientes, los bytes entregados por el asignador se consideran técnicamente "no especificados". Debe suponer que se trata de datos basura (cualquier cosa). No haga suposiciones sobre el contenido.

Las compilaciones de depuración con Visual Studio a menudo inicializan buffers con valores 0xcc o 0xcd, pero ese no es el caso en las compilaciones de lanzamiento. Sin embargo, existen indicadores de compilación y técnicas de asignación de memoria para Windows y Visual Studio donde puede garantizar asignaciones de memoria de inicio cero, pero no es portátil.

17
selbie 18 sep. 2018 a las 04:16

C ++ tiene clases de almacenamiento que incluyen global, automática y estática. La inicialización depende de cómo se declare la variable.

char global[12];  // all 0
static char s_global[12]; // all 0

void foo()
{
   static char s_local[12]; // all 0
   char local[12]; // automatic storage variables are uninitialized, accessing before initialization is undefined behavior 
}

Algunos detalles interesantes aquí.

11
Matthew Fisher 2 oct. 2018 a las 12:22

En general, no es del todo inusual que las memorias intermedias sean insuficientes. A menudo es una buena práctica asignar buffers más grandes de lo necesario. (Intentar calcular siempre un tamaño de búfer exacto es una fuente frecuente de error y, a menudo, una pérdida de tiempo).

Cuando un búfer es más grande de lo necesario, cuando el búfer contiene menos datos que su tamaño asignado, obviamente es importante realizar un seguimiento de la cantidad de datos que hay allí. En general, hay dos formas de hacerlo: (1) con un recuento explícito, mantenido en una variable separada, o (2) con un valor "centinela", como el carácter \0 que marca el final de un cuerda en C.

Pero luego está la pregunta, si no se está utilizando todo un búfer, ¿qué contienen las entradas no utilizadas?

Una respuesta es, por supuesto, que no importa . Eso es lo que significa "sin usar". Le interesan los valores de las entradas que se utilizan, que se contabilizan por su recuento o su valor centinela. No te importan los valores no utilizados.

Básicamente, existen cuatro situaciones en las que puede predecir los valores iniciales de las entradas no utilizadas en un búfer:

  1. Cuando asigna una matriz (incluida una matriz de caracteres) con una duración static, todas las entradas no utilizadas se inicializan a 0.

  2. Cuando asigna una matriz y le da un inicializador explícito, todas las entradas no utilizadas se inicializan a 0.

  3. Cuando llama a calloc, la memoria asignada se inicializa a all-bits-0.

  4. Cuando llama a strncpy, la cadena de destino se rellena al tamaño n con \0 caracteres.

En todos los demás casos, las partes no utilizadas de un búfer son impredecibles y generalmente contienen lo que hicieron la última vez (lo que sea que eso signifique). En particular, no puede predecir el contenido de una matriz no inicializada con duración automática (es decir, una que es local para una función y no se declara con static), y no puede predecir el contenido de la memoria obtenida con {{ X1}}. (Algunas veces, en esos dos casos, la memoria tiende a comenzar como cero bits la primera vez, pero definitivamente no quieres depender de esto).

1
Steve Summit 17 sep. 2018 a las 11:52

Depende del especificador de clase de almacenamiento, su implementación y su configuración. Algunos ejemplos interesantes: - Las variables de pila no inicializadas se pueden establecer en 0xCCCCCCCC - Las variables de montón no inicializadas se pueden establecer en 0xCDCDCDCD - Las variables estáticas o globales no inicializadas se pueden establecer en 0x00000000 - O podría ser basura. Es arriesgado hacer suposiciones sobre esto.

1
Tim Randall 17 sep. 2018 a las 16:07

Los objetos declarados de duración estática (aquellos declarados fuera de una función, o con un calificador static) que no tienen un inicializador especificado se inicializan a cualquier valor que esté representado por un cero literal [es decir un entero cero, un punto flotante cero o un puntero nulo, según corresponda, o una estructura o unión que contenga dichos valores]. Si la declaración de cualquier objeto (incluidos los de duración automática) incluye un inicializador, las partes cuyos valores se especifican por ese inicializador se establecerán como se especifica, y el resto se pondrá a cero como con los objetos estáticos.

Para objetos automáticos sin inicializadores, la situación es algo más ambigua. Dado algo como:

#include <string.h>

unsigned char static1[5], static2[5];

void test(void)
{
  unsigned char temp[5];
  strcpy(temp, "Hey");
  memcpy(static1, temp, 5);
  memcpy(static2, temp, 5);
}

El Estándar es claro que test no invocaría Comportamiento indefinido, aunque copie porciones de temp que no se inicializaron. El texto de la Norma, al menos a partir de C11, no está claro en cuanto a si hay algo garantizado acerca de los valores de static1[4] y static2[4], especialmente si pueden quedar con valores diferentes. Un informe de defectos indica que el Estándar no tenía la intención de prohibir que un compilador se comportara como si el código hubiera sido:

unsigned char static1[5]={1,1,1,1,1}, static2[5]={2,2,2,2,2};

void test(void)
{
  unsigned char temp[4];
  strcpy(temp, "Hey");
  memcpy(static1, temp, 4);
  memcpy(static2, temp, 4);
}

Que podría dejar static1[4] y static2[4] con diferentes valores. La Norma no dice si los compiladores de calidad destinados a diversos fines deberían comportarse en esa función. El Estándar tampoco ofrece orientación sobre cómo se debe escribir la función si la intención si el programador requiere que static1[4] y static2[4] tengan el mismo valor, pero no le importa cuál sea ese valor.

1
chqrlie 18 sep. 2018 a las 03:48

Todas las respuestas anteriores son muy buenas y muy detalladas, pero el OP parece ser nuevo en la programación en C. Entonces, pensé que un ejemplo de Mundo real podría ser útil.

Imagine que tiene un soporte de bebidas de cartón que puede contener seis botellas. Ha estado sentado en su garaje, por lo que en lugar de seis botellas, contiene varias cosas desagradables que se acumulan en las esquinas de los garajes: arañas, casas de ratones, etc.

Un búfer de computadora es un poco así justo después de asignarlo. Realmente no puedes estar seguro de lo que contiene, solo sabes lo grande que es.

Ahora, digamos que pones cuatro botellas en tu soporte. Su soporte no ha cambiado de tamaño, pero ahora sabe qué hay en cuatro de los espacios. Los otros dos espacios, completos con sus contenidos cuestionables, todavía están allí.

Los tampones de computadora son de la misma manera. Es por eso que con frecuencia ve una variable bufferSize para rastrear cuánto del búfer está en uso. Un nombre mejor podría ser numberOfBytesUsedInMyBuffer pero los programadores tienden a ser terriblemente concisos.

3
Doug Clutter 17 sep. 2018 a las 14:41

El programa conoce la longitud de una cadena porque la termina con un terminador nulo, un carácter de valor cero.

Es por eso que para ajustar una cadena en un búfer, el búfer debe ser al menos 1 carácter más largo que el número de caracteres en la cadena, para que pueda caber la cadena más el terminador nulo también.

Cualquier espacio después de eso en el búfer se deja intacto. Si había datos allí anteriormente, todavía está allí. Esto es lo que llamamos basura.

Es incorrecto suponer que este espacio está lleno de cero solo porque aún no lo ha utilizado, no sabe para qué se utilizó ese espacio de memoria en particular antes de que su programa llegara a ese punto. La memoria no inicializada debe manejarse como si lo que contiene es aleatorio y poco confiable.

4
Havenard 17 sep. 2018 a las 02:21