Sé que un "comportamiento indefinido" en C ++ puede permitir que el compilador haga lo que quiera. Sin embargo, tuve un bloqueo que me sorprendió, ya que asumí que el código era lo suficientemente seguro.
En este caso, el problema real ocurrió solo en una plataforma específica usando un compilador específico, y solo si la optimización estaba habilitada.
Probé varias cosas para reproducir el problema y simplificarlo al máximo. Aquí hay un extracto de una función llamada Serialize
, que tomaría un parámetro bool y copiaría la cadena true
o false
en un búfer de destino existente.
¿Esta función estaría en una revisión de código, no habría forma de saber que, de hecho, podría fallar si el parámetro bool fuera un valor no inicializado?
// Zero-filled global buffer of 16 characters
char destBuffer[16];
void Serialize(bool boolValue) {
// Determine which string to print based on boolValue
const char* whichString = boolValue ? "true" : "false";
// Compute the length of the string we selected
const size_t len = strlen(whichString);
// Copy string into destination buffer, which is zero-filled (thus already null-terminated)
memcpy(destBuffer, whichString, len);
}
Si este código se ejecuta con optimizaciones clang 5.0.0 +, puede fallar.
El operador ternario esperado boolValue ? "true" : "false"
parecía lo suficientemente seguro para mí, estaba asumiendo, "Cualquiera que sea el valor de basura boolValue
no importa, ya que se evaluará como verdadero o falso de todos modos".
He configurado un ejemplo de Compiler Explorer que muestra el problema en el desmontaje, aquí el ejemplo completo. Nota: para reproducir el problema, la combinación que encontré que funcionó es usar Clang 5.0.0 con optimización -O2.
#include <iostream>
#include <cstring>
// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
bool uninitializedBool;
__attribute__ ((noinline)) // Note: the constructor must be declared noinline to trigger the problem
FStruct() {};
};
char destBuffer[16];
// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
// Determine which string to print depending if 'boolValue' is evaluated as true or false
const char* whichString = boolValue ? "true" : "false";
// Compute the length of the string we selected
size_t len = strlen(whichString);
memcpy(destBuffer, whichString, len);
}
int main()
{
// Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
FStruct structInstance;
// Output "true" or "false" to stdout
Serialize(structInstance.uninitializedBool);
return 0;
}
El problema surge debido al optimizador: fue lo suficientemente inteligente para deducir que las cadenas "verdadero" y "falso" solo difieren en longitud en 1. Por lo tanto, en lugar de calcular realmente la longitud, utiliza el valor del bool en sí, que debería técnicamente ser 0 o 1, y es así:
const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue; // clang clever optimization
Si bien esto es "inteligente", por así decirlo, mi pregunta es: ¿El estándar C ++ permite que un compilador asuma que un bool solo puede tener una representación numérica interna de '0' o '1' y usarlo de esa manera?
¿O es este un caso de implementación definida, en cuyo caso la implementación asumió que todos sus bools solo contendrán 0 o 1, y cualquier otro valor es un territorio de comportamiento indefinido?
Pero también tenga en cuenta que ISO C ++ permite que un compilador emita código que se bloquee a propósito (por ejemplo, con una instrucción ilegal) si el programa encuentra UB, por ejemplo, como una forma de ayudarlo a encontrar errores. (O porque es una DeathStation 9000. Ser estrictamente conforme no es suficiente para que una implementación de C ++ sea útil para cualquier propósito real). Entonces, ISO C ++ permitiría a un compilador hacer un ensamblaje que fallara (por razones totalmente diferentes) incluso en un código similar que lee un archivo uint32_t
. Aunque se requiere que sea un tipo de diseño fijo sin representaciones de trampas.
Es una pregunta interesante sobre cómo funcionan las implementaciones reales, pero recuerde que incluso si la respuesta fuera diferente, su código no sería seguro porque C ++ moderno no es una versión portátil del lenguaje ensamblador.
Está compilando para el ¿Dónde se documenta la ABI de System V x86-64? , que especifica que un bool
argumento como función en un registro está representado por los patrones de bits false=0
ytrue=1
en los 8 bits bajos del registro 1 . En memoria, bool
es un tipo de 1 byte que nuevamente debe tener un valor entero de 0 o 1.
(Una ABI es un conjunto de opciones de implementación que los compiladores de la misma plataforma acuerdan para que puedan crear código que llame a las funciones de los demás, incluidos los tamaños de tipo, las reglas de diseño de estructuras y las convenciones de llamada).
ISO C ++ no lo especifica, pero esta decisión ABI está muy extendida porque hace que la conversión bool-> int sea barata (solo extensión cero) . No conozco ninguna ABI que no permita que el compilador asuma 0 o 1 bool
para cualquier arquitectura (no solo x86). Permite optimizaciones como !mybool
con xor eax,1
para cambiar el bit bajo: Cualquier código posible que pueda cambiar un bit / entero / bool entre 0 y 1 en una sola instrucción de CPU . O compilando a&&b
a bit a bit AND para bool
tipos. Algunos compiladores realmente aprovechan los valores booleanos como 8 bits en los compiladores. ¿Son ineficaces las operaciones sobre ellos? .
En general, la regla como si permite que el compilador aproveche las cosas que son verdaderas en la plataforma de destino para la que se está compilando , porque el resultado final será un código ejecutable que implemente el mismo comportamiento visible externamente que la fuente C ++. (Con todas las restricciones que Undefined Behavior impone a lo que en realidad es "visible externamente": no con un depurador, sino desde otro hilo en un programa C ++ legal / bien formado).
El compilador es, sin duda permitió a sacar el máximo provecho de una garantía ABI en su código-gen, y hacer que el código que has encontrado lo que optimiza strlen(whichString)
a
5U - boolValue
. (Por cierto, esta optimización es algo inteligente, pero tal vez miope frente a la ramificación e inserción memcpy
como almacenes de datos inmediatos 2 ).
O el compilador podría haber creado una tabla de punteros y haberla indexado con el valor entero de bool
, nuevamente asumiendo que era un 0 o 1. ( ¿El estándar C ++ permite que un bool no inicializado bloquee un programa? ).
Su __attribute((noinline))
constructor con la optimización habilitada provocó un sonido metálico con solo cargar un byte de la pila para usarlo como uninitializedBool
. Hizo espacio para el objeto main
con push rax
(que es más pequeño y por varias razones tan eficiente como sub rsp, 8
), por lo que cualquier basura que haya en AL al ingresar main
es el valor para el que se usó uninitializedBool
. Es por eso que en realidad obtuviste valores que no eran justos 0
.
5U - random garbage
puede ajustarse fácilmente a un valor sin firmar grande, lo que lleva a memcpy a entrar en la memoria sin asignar. El destino está en el almacenamiento estático, no en la pila, por lo que no está sobrescribiendo una dirección de retorno o algo así.
Otras implementaciones podrían hacer elecciones diferentes, por ejemplo false=0
y true=any non-zero value
. Entonces clang probablemente no generaría código que fallara para esta instancia específica de UB. (Pero aún estaría permitido si quisiera). No conozco ninguna implementación que elija otra cosa para lo que hace x86-64 bool
, pero el estándar C ++ permite muchas cosas que nadie hace o incluso querría hacer en hardware que se parezca a las CPU actuales.
ISO C ++ deja sin especificar lo que encontrará cuando examine o modifique la representación del objeto de unbool
. (por ejemplo, memcpy
ingresando bool
en unsigned char
, lo cual puede hacer porque char*
puede usar un alias para cualquier cosa. Y unsigned char
se garantiza que no tiene bits de relleno, por lo que el estándar C ++ le permite formalmente representaciones de objetos hexdump sin ningún UB. Conversión de punteros para copiar el objeto la representación es diferente de la asignación char foo = my_bool
, por supuesto, por lo que la booleanización a 0 o 1 no sucedería y obtendría la representación del objeto sin formato).
Ha "ocultado" parcialmente el UB en esta ruta de ejecución del compilador connoinline
. Sin embargo, incluso si no está en línea, las optimizaciones entre procedimientos aún podrían crear una versión de la función que dependa de la definición de otra función. (Primero, clang es hacer un ejecutable, no una biblioteca compartida de Unix donde puede ocurrir la interposición de símbolos. En segundo lugar, la definición dentro de la class{}
definición para que todas las unidades de traducción deben tener la misma definición. Como con la inline
palabra clave).
Entonces, un compilador podría emitir solo una ret
o ud2
(instrucción ilegal) como la definición de main
, porque la ruta de ejecución que comienza en la parte superior de main
inevitablemente encuentra un comportamiento indefinido. (Que el compilador puede ver en tiempo de compilación si decide seguir la ruta a través del constructor no en línea).
Cualquier programa que se encuentre con UB está totalmente indefinido durante toda su existencia. Pero UB dentro de una función o if()
rama que en realidad nunca se ejecuta no corrompe el resto del programa. En la práctica, eso significa que los compiladores pueden decidir emitir una instrucción ilegal, o a ret
, o no emitir nada y caer en el siguiente bloque / función, para todo el bloque básico que se puede probar en tiempo de compilación para contener o conducir a UB.
CCG y Sonido metálico en la práctica hacen realidad a veces emiten ud2
en la UB, en lugar de intentar siquiera para generar código para rutas de ejecución que no tienen sentido. O para casos como caer al final de una no void
función, gcc a veces omitirá una ret
instrucción. Si pensaba que "mi función volverá con cualquier basura que haya en RAX", está muy equivocado. Los compiladores modernos de C ++ ya no tratan el lenguaje como un lenguaje ensamblador portátil. Su programa realmente tiene que ser válido en C ++, sin hacer suposiciones sobre cómo se vería una versión independiente no en línea de su función en asm.
Otro ejemplo divertido es ¿Por qué el acceso no alineado a la memoria mmap a veces se produce por defecto en AMD64?. x86 no falla en los enteros no alineados, ¿verdad? Entonces, ¿por qué un desalineado uint16_t*
sería un problema? Porque alignof(uint16_t) == 2
, y violar esa suposición llevó a un error de segmentación al auto-vectorizar con SSE2.
Consulte también Lo que todo programador de C debe saber sobre el comportamiento indefinido # 1/3 , un artículo de un desarrollador clang.
bool
.Espere una hostilidad total hacia muchos errores por parte del programador, especialmente cosas sobre las que advierten los compiladores modernos. Es por eso que debe usar -Wall
y corregir las advertencias. C ++ no es un lenguaje fácil de usar, y algo en C ++ puede ser inseguro incluso si fuera seguro en el conjunto del destino para el que está compilando. (por ejemplo, el desbordamiento firmado es UB en C ++ y los compiladores asumirán que no sucede, incluso al compilar para el complemento x86 de 2, a menos que lo use clang/gcc -fwrapv
).
La UB visible en tiempo de compilación siempre es peligrosa, y es realmente difícil estar seguro (con la optimización del tiempo de enlace) de que realmente ha ocultado la UB del compilador y, por lo tanto, puede razonar sobre qué tipo de asm generará.
No ser demasiado dramático; A menudo, los compiladores te permiten salirte con la tuya en algunas cosas y emitir código como esperabas incluso cuando algo es UB. Pero tal vez sea un problema en el futuro si los desarrolladores de compiladores implementan alguna optimización que obtenga más información sobre rangos de valores (por ejemplo, que una variable no sea negativa, tal vez permitiéndole optimizar la extensión de signo para liberar la extensión cero en x86- 64). Por ejemplo, en gcc y clang actuales, hacer tmp = a+INT_MIN
no se optimiza a<0
como siempre falso, solo que tmp
siempre es negativo. (Porque INT_MIN
+ a=INT_MAX
es negativo en el objetivo de complemento a 2 y a
no puede ser más alto que eso).
Por lo tanto, gcc / clang no retrocede actualmente para derivar información de rango para las entradas de un cálculo, solo en los resultados basados en la suposición de que no hay desbordamiento firmado: ejemplo en Godbolt . No sé si esta optimización se "pierde" intencionalmente en nombre de la facilidad de uso o qué.
También tenga en cuenta que las implementaciones (también conocidas como compiladores) pueden definir el comportamiento que ISO C ++ deja sin definir . Por ejemplo, todos los compiladores que admiten los elementos intrínsecos de Intel (como _mm_add_ps(__m128, __m128)
para la vectorización SIMD manual) deben permitir la formación de punteros mal alineados, que es UB en C ++ incluso si no los elimina. __m128i _mm_loadu_si128(const __m128i *)
realiza cargas desalineadas tomando un __m128i*
argumento desalineado , no un void*
o char*
. ¿Es la `reinterpretación_cast` entre el puntero vectorial de hardware SIMD y el tipo correspondiente un comportamiento indefinido?
GNU C / C ++ también define el comportamiento de desplazamiento a la izquierda de un número con signo negativo (incluso sin él -fwrapv
), por separado de las reglas UB normales de desbordamiento con signo. ( ¿Por qué la operación de desplazamiento a la izquierda invoca un comportamiento indefinido cuando el operando del lado izquierdo tiene un valor negativo? , mientras que los desplazamientos a la derecha de los números con signo están definidos por la implementación (lógico frente a aritmético); las implementaciones de buena calidad eligen aritmética en HW que tiene desplazamientos aritméticos a la derecha, pero ISO C ++ no especifica). Esto está documentado en la sección Integer del manual de GCC , junto con la definición del comportamiento definido por la implementación que los estándares C requieren que las implementaciones definan de una forma u otra.
Definitivamente hay problemas de calidad de implementación que preocupan a los desarrolladores de compiladores; generalmente no están tratando de hacer compiladores que sean intencionalmente hostiles, pero aprovechar todos los baches de UB en C ++ (excepto los que eligen definir) para optimizar mejor puede ser casi indistinguible a veces.
Nota al pie 1 : Los 56 bits superiores pueden ser basura que el destinatario de la llamada debe ignorar, como es habitual en los tipos más estrechos que un registro.
( Otros ABIs hacen tomar decisiones diferentes aquí . Algunos no requieren tipos enteros estrechas ser cero o signo extendido para llenar un registro o cuando se pasa a regresar de funciones, como MIPS64 y PowerPC64. Ver la última sección de MOVZX falta registro de 32 bits a registro de 64 bits ).
Por ejemplo, una persona que llama podría haber calculado a & 0x01010101
en RDI y haberlo usado para otra cosa antes de llamar bool_func(a&1)
. La persona que llama podría optimizar &1
porque ya lo hizo con el byte bajo como parte de and edi, 0x01010101
, y sabe que el destinatario de la llamada debe ignorar los bytes altos.
O si se pasa un bool como tercer argumento, tal vez un llamador que optimiza el tamaño del código lo carga con, en mov dl, [mem]
lugar de movzx edx, [mem]
, guardar 1 byte a costa de una dependencia falsa del valor anterior de RDX (u otro efecto de registro parcial, dependiendo en el modelo de CPU). O para el primer argumento, en mov dil, byte [r10]
lugar de movzx edi, byte [r10]
, porque ambos requieren un prefijo REX de todos modos.
Es por eso que clang emite movzx eax, dil
dentro Serialize
, en lugar de sub eax, edi
. (Para args enteros, clang viola esta regla ABI, en cambio, depende del comportamiento indocumentado de gcc y clang a cero o signo-extender enteros estrechos a 32 bits. ¿Se requiere un signo o una extensión cero al agregar un desplazamiento de 32 bits a un puntero para el ABI x86-64?
Así que estaba interesado en ver que no hace lo mismo para bool
.)
Nota al pie 2: después de la bifurcación, solo tendrá un mov
almacén de 4 bytes inmediato o de 4 bytes + 1 byte. La longitud está implícita en los anchos de la tienda + compensaciones.
OTOH, glibc memcpy hará dos cargas / almacenes de 4 bytes con una superposición que depende de la longitud, por lo que esto realmente termina haciendo que todo esté libre de ramas condicionales en el booleano. Vea el L(between_4_7):
bloque en memcpy / memmove de glibc. O al menos, siga el mismo camino para cualquier booleano en la ramificación de memcpy para seleccionar un tamaño de fragmento.
Si está en línea, puede usar 2x mov
-immediate + cmov
y un desplazamiento condicional, o puede dejar los datos de la cadena en la memoria.
O si está sintonizando Intel Ice Lake ( con la función Fast Short REP MOV ), un real rep movsb
podría ser óptimo. glibc memcpy
podría comenzar a usarse rep movsb
para tamaños pequeños en CPU con esa función, ahorrando muchas ramificaciones.
En gcc y clang, puede compilar con -fsanitize=undefined
para agregar instrumentación en tiempo de ejecución que advertirá o producirá un error en UB que ocurra en tiempo de ejecución. Sin embargo, eso no capturará variables unitarias. (Porque no aumenta los tamaños de letra para dejar espacio para un bit "no inicializado").
Ver https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/
Para encontrar el uso de datos no inicializados, hay Address Sanitizer y Memory Sanitizer en clang / LLVM. https://github.com/google/sanitizers/wiki/MemorySanitizer muestra ejemplos de clang -fsanitize=memory -fPIE -pie
detección de lecturas de memoria no inicializadas. Podría funcionar mejor si compila sin optimización, por lo que todas las lecturas de variables terminan cargándose desde la memoria en el asm. Muestran que se usa -O2
en un caso en el que la carga no se optimizaría. Yo no lo he probado. (En algunos casos, por ejemplo, no inicializar un acumulador antes de sumar una matriz, clang -O3 emitirá un código que se suma en un registro vectorial que nunca se inicializó. Por lo tanto, con la optimización, puede tener un caso en el que no hay lectura de memoria asociada con la UB . Pero -fsanitize=memory
cambia el asm generado y puede resultar en una verificación para esto.)
Tolerará la copia de memoria no inicializada y también operaciones lógicas y aritméticas simples con él. En general, MemorySanitizer rastrea silenciosamente la propagación de datos no inicializados en la memoria e informa de una advertencia cuando se toma (o no se toma) una rama de código en función de un valor no inicializado.
MemorySanitizer implementa un subconjunto de funciones que se encuentran en Valgrind (herramienta Memcheck).
Debería funcionar para este caso porque la llamada a glibc memcpy
con un length
cálculo de memoria no inicializada resultará (dentro de la biblioteca) en una rama basada en length
. Si hubiera incorporado una versión completamente sin sucursales que solo usara cmov
, indexación y dos tiendas, es posible que no hubiera funcionado.
Valgrind'smemcheck
también buscará este tipo de problema, nuevamente sin quejarse si el programa simplemente copia datos no inicializados. Pero dice que detectará cuando un "salto o movimiento condicional depende de valores no inicializados", para tratar de detectar cualquier comportamiento visible externamente que dependa de datos no inicializados.
Quizás la idea detrás de no marcar solo una carga es que las estructuras pueden tener relleno, y copiar toda la estructura (incluido el relleno) con una carga / almacenamiento de vector amplio no es un error, incluso si los miembros individuales solo se escribieron uno a la vez. A nivel de conjunto, se ha perdido la información sobre lo que se estaba rellenando y lo que en realidad es parte del valor.
El compilador puede asumir que un valor booleano pasado como argumento es un valor booleano válido (es decir, uno que ha sido inicializado o convertido a true
o false
). El true
valor no tiene que ser el mismo que el entero 1 - de hecho, puede haber varias representaciones de true
y false
- pero el parámetro debe ser una representación válida de uno de esos dos valores, donde "representación válida" es implementación- definido.
Entonces, si no puede inicializar a bool
, o si logra sobrescribirlo a través de algún puntero de un tipo diferente, las suposiciones del compilador serán incorrectas y se producirá un comportamiento indefinido. Le habían advertido:
50) El uso de un valor bool de las formas descritas por esta Norma Internacional como "indefinido", como al examinar el valor de un objeto automático no inicializado, puede hacer que se comporte como si no fuera verdadero ni falso. (Nota a pie de página del párrafo 6 de §6.9.1, Tipos fundamentales)
La función en sí es correcta, pero en su programa de prueba, la declaración que llama a la función provoca un comportamiento indefinido al usar el valor de una variable no inicializada.
El error está en la función de llamada y podría detectarse mediante la revisión del código o el análisis estático de la función de llamada. Usando el enlace del explorador de su compilador, el compilador gcc 8.2 detecta el error. (Tal vez podría presentar un informe de error contra clang que no encuentra el problema).
El comportamiento indefinido significa que puede suceder cualquier cosa , lo que incluye que el programa se bloquee unas pocas líneas después del evento que desencadenó el comportamiento indefinido.
NÓTESE BIEN. La respuesta a "¿Puede el comportamiento indefinido causar _____?" es siempre "Sí". Esa es literalmente la definición de comportamiento indefinido.
Un bool solo puede contener los valores dependientes de la implementación usados internamente para true
y false
, y el código generado puede asumir que solo contendrá uno de estos dos valores.
Normalmente, la implementación utilizará el entero 0
for false
y 1
for true
, para simplificar las conversiones entre bool
y int
, y if (boolvar)
generar el mismo código que if (intvar)
. En ese caso, uno puede imaginar que el código generado para el ternario en la asignación usaría el valor como índice en una matriz de punteros a las dos cadenas, es decir, podría convertirse en algo como:
// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];
Si boolValue
no está inicializado, en realidad podría contener cualquier valor entero, lo que provocaría el acceso fuera de los límites de la strings
matriz.
Resumiendo mucho su pregunta, está preguntando: ¿El estándar C ++ permite que un compilador asuma bool
que solo puede tener una representación numérica interna de '0' o '1' y usarlo de tal manera?
El estándar no dice nada sobre la representación interna de a bool
. Solo define lo que sucede cuando se lanza un bool
a un int
(o viceversa). Principalmente, debido a estas conversiones integrales (y al hecho de que la gente depende mucho de ellas), el compilador usará 0 y 1, pero no tiene que hacerlo (aunque tiene que respetar las restricciones de cualquier ABI de nivel inferior que use ).
Entonces, el compilador, cuando ve un, bool
tiene derecho a considerar que dicho bool
contiene cualquiera de los patrones de bits true
"o" false
y hacer lo que le parezca. Así que si los valores de true
y false
son 1 y 0, respectivamente, el compilador se permite de hecho a optimizar el strlen
a 5 - <boolean value>
. ¡Son posibles otros comportamientos divertidos!
Como se indica repetidamente aquí, el comportamiento indefinido tiene resultados indefinidos. Incluyendo pero no limitado a
Vea lo que todo programador debe saber sobre el comportamiento indefinido
La estrella de HGTV, Christina Hall, revela que le diagnosticaron envenenamiento por mercurio y plomo, probablemente debido a su trabajo como manipuladora de casas.
Recientemente salió a la luz un informe policial que acusa a la estrella de 'Love Is Blind', Brennon, de violencia doméstica. Ahora, Brennon ha respondido a los reclamos.
Conozca cómo Wynonna Judd se dio cuenta de que ahora es la matriarca de la familia mientras organizaba la primera celebración de Acción de Gracias desde que murió su madre, Naomi Judd.
Descubra por qué un destacado experto en lenguaje corporal cree que es fácil trazar "tales paralelismos" entre la princesa Kate Middleton y la princesa Diana.
Los inodoros arrojan columnas de aerosol invisibles con cada descarga. ¿Como sabemos? La prueba fue capturada por láseres de alta potencia.
Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?
The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!
¿Sigue siendo efectivo ese lote de repelente de insectos que te quedó del verano pasado? Si es así, ¿por cuánto tiempo?
Anteriormente, Kotaku informó que un hotel Godzilla se estaba abriendo en Tokio este abril. Junto al hotel, estaba programada la aparición de una enorme cabeza de 'Zilla, pero todo lo que hemos visto fueron imágenes conceptuales computarizadas.
Foto: Getty Desde que lanzó The Boring Company hace un año, Elon Musk ha mencionado varios sitios de construcción posibles para el negocio de perforación de túneles y ha descartado una vaga referencia a una aprobación gubernamental "verbal" para un túnel Hyperloop que conecta la ciudad de Nueva York y Washington. , CC. Pero ahora sabemos que al menos un alcalde quiere que Musk perfore un agujero debajo de su ciudad.
Tapas elásticas de silicona de Tomorrow's Kitchen, paquete de 12 | $14 | Amazonas | Código promocional 20OFFKINJALids son básicamente los calcetines de la cocina; siempre perdiéndose, dejando contenedores huérfanos que nunca podrán volver a cerrarse. Pero, ¿y si sus tapas pudieran estirarse y adaptarse a todos los recipientes, ollas, sartenes e incluso frutas en rodajas grandes que sobran? Nunca más tendrás que preocuparte por perder esa tapa tan específica.
Hemos pirateado algunas ciudades industriales en esta columna, como Los Ángeles y Las Vegas. Ahora es el momento de una ciudad militar-industrial-compleja.
El equipo está a la espera de las medallas que ganó en los Juegos Olímpicos de Invierno de 2022 en Beijing, ya que se está resolviendo un caso de dopaje que involucra a la patinadora artística rusa Kamila Valieva.
Miles de compradores de Amazon recomiendan la funda de almohada de seda Mulberry, y está a la venta en este momento. La funda de almohada de seda viene en varios colores y ayuda a mantener el cabello suave y la piel clara. Compre las fundas de almohada de seda mientras tienen hasta un 46 por ciento de descuento en Amazon
El jueves se presentó una denuncia de delito menor amenazante agravado contra Joe Mixon.
El Departamento de Policía de Lafayette comenzó a investigar a un profesor de la Universidad de Purdue en diciembre después de recibir varias denuncias de un "hombre sospechoso que se acercaba a una mujer".
Al igual que el mundo que nos rodea, el lenguaje siempre está cambiando. Mientras que en eras anteriores los cambios en el idioma ocurrían durante años o incluso décadas, ahora pueden ocurrir en cuestión de días o incluso horas.
Estoy de vuelta por primera vez en seis años. No puedo decirte cuánto tiempo he estado esperando esto.
“And a river went out of Eden to water the garden, and from thence it was parted and became into four heads” Genesis 2:10. ? The heart is located in the middle of the thoracic cavity, pointing eastward.
Creo, un poco tarde en la vida, en dar oportunidades a la gente. Generosamente.