Solución del problema que provoca que se utilice el formateador no coincidente %d al formatear datos enteros UINT64, lo que provoca que otros parámetros no se puedan imprimir.

Tabla de contenido

1. Descripción del problema

2. Descripción completa del mecanismo del análisis interno de los parámetros a formatear de la función de formateo.

2.1 Los parámetros pasados ​​a la función llamada se pasan a través de la pila.

2.2 ¿Cómo encuentra la función de formato el valor del parámetro que se va a formatear en la pila y completa el formateo?

2.3 Descripción de escenarios de problemas anormales correspondientes al formateador de cadenas %s

2.4 Para facilitar la comprensión del mecanismo anterior, se adjunta el código fuente de implementación de la función Formato de la clase CString en VC6.0.

2.5 Si desea formatear los datos de un objeto de clase C++ y el objeto contiene varios miembros de datos, debe especificar claramente el miembro de datos que se va a formatear.

3. Análisis de problemas y resolución de problemas en este caso.

3.1 Código de problema

3.2 Análisis preliminar

3.3 ¿Por qué hay problemas al utilizar el formateador %d para datos UINT64?

3.4 Solución

4. Finalmente


Resumen del desarrollo de funciones comunes de VC ++ (lista de artículos de columna, bienvenido a suscribirse, actualizaciones continuas ...) icono-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585 Serie de tutoriales de solución de problemas de anomalías del software C ++ de entrada a la competencia (lista de artículos de la columna), bienvenido a suscribirse y continuar actualizando...) icono-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931 Herramientas de análisis de software C ++ desde la entrada hasta la recopilación de casos de dominio (columna El artículo se está actualizando...) icono-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795 Conceptos básicos y avanzados de C/C++ (artículo en columna, actualizado continuamente...) icono-default.png?t=N7T8https://blog.csdn.net /chenlycly/category_11931267.html        Los colegas están analizando Al solucionar problemas comerciales en el registro de ejecución del software, descubrí que el valor de ciertos datos clave no se imprimió, así que verifiqué el código para imprimir el registro, pero no se encontró ningún problema. Un colega vino a verme y me pidió que le ayudara a analizarlo y descubrir qué lo causaba. Después de un análisis en profundidad, se descubrió que este problema se debía a una excepción al analizar los datos de parámetros entrantes de la memoria de la pila dentro de la función de formateo. El código utilizaba incorrectamente el carácter de formato %d para los datos enteros UINT64 a formatear . Hoy describiremos en detalle el proceso de solución de este problema.

1. Descripción del problema

       Busque el código correspondiente para imprimir registros en el código fuente, como se muestra a continuación:

Ahora formatee la información que se imprimirá en una cadena y luego imprima la cadena. En el código anterior se imprimen cinco parámetros ( strId.c_str() ). Estos parámetros están formateados en la cadena de destino. Al observar el registro, se encuentra que el quinto parámetro no está impreso. Entre ellos, el primer parámetro ( strId.c_str() ) es la primera dirección de la cadena, el segundo parámetro ( el valor de retorno de la función tRtcPlayItem.play_idx() ) es un valor entero UINT64 y el tercer y cuarto parámetro son Ambos son de tipo bool, el quinto parámetro ( strId.c_str() ) también es la primera dirección de la cadena. El quinto parámetro es la primera dirección de la cadena y hay una cadena válida en la memoria a la que se apunta.

       Generalmente, este tipo de problema de formato de datos suele estar causado por una falta de coincidencia entre los datos a formatear y el formateador correspondiente, lo que generalmente conduce a dos tipos de problemas. En primer lugar, los datos impresos son anormales o no se pueden imprimir y, en segundo lugar, cuando la función de formateo obtiene datos de parámetros de la pila, accede a una memoria a la que no debería acceder, lo que provoca una infracción de acceso a la memoria y provoca un bloqueo.

       Básicamente, todo el mundo sabe que cuando el formateador no coincide con el tipo de parámetro a formatear, pueden ocurrir problemas o el software puede fallar de manera anormal. Sin embargo, no mucha gente sabe por qué ocurren los problemas y la causa raíz de los mismos. Este artículo lo llevará a explorar las causas profundas en detalle, dominar el contenido de este artículo y la próxima vez que encuentre problemas de formato, podrá analizarlos y solucionarlos usted mismo.

2. Descripción completa del mecanismo del análisis interno de los parámetros a formatear de la función de formateo.

2.1 Los parámetros pasados ​​a la función llamada se pasan a través de la pila.

      Generalmente, al llamar a una función, los parámetros pasados ​​a la función llamada se insertan en la pila y se pasan a la función llamada, es decir, los parámetros entrantes se pasan a la función llamada a través de la pila. Puede ver esto más claramente mirando el código ensamblador, como el siguiente código : (Llame a la función AddNum que agrega dos datos int, rompa el punto en la llamada a la función, haga clic derecho después de alcanzar el punto de interrupción y salte al Ensamblaje inverso, ver código ensamblador )

Como se muestra arriba, antes de llamar a AddNum, inserte los parámetros a y b para pasarlos a la pila respectivamente y luego llame a la función AddNum. Para la función llamada AddNum, vaya a la pila para obtener el valor del parámetro pasado.

Por supuesto, pasar parámetros a través de la pila no es absoluto. Por ejemplo, para programas de 64 bits, hay más registros disponibles. Cuando no hay muchos parámetros para formatear, los registros se pueden pasar directamente. Esto es más eficiente que enviar parámetros a través de la pila. Memoria de pila y lectura de la memoria de pila, mucho más rápida. El siguiente párrafo está extraído de la descripción oficial de Microsoft:

En los procesadores ARM y x64, se aceptan convenciones de llamada como __cdecl y __stdcall, pero el compilador generalmente las ignora. Según la convención de ARM y x64, los argumentos se pasan a los registros cuando es posible y los argumentos posteriores se pasan a la pila.

Para obtener instrucciones detalladas sobre el paso de parámetros al llamar a funciones en el entorno x64, si está interesado, puede consultar el enlace oficial de Microsoft: https://learn.microsoft.com/zh-cn/cpp/cpp/argument-passing-and -nombramiento -convenciones?view=msvc-170

Como se mencionó anteriormente, ya sea x86 o x64, puede ver directamente el código de desensamblado en Visual Studio y sabrá cómo se pasan los parámetros.

       ¿Por qué necesitamos hablar sobre cómo se pasan los parámetros a la función llamada? Esto está directamente relacionado con cómo la función de formato obtiene el contenido del parámetro pasado de la pila. Para comprender cómo se leen los parámetros entrantes desde la pila dentro de la función de formato, debe comprender la distribución de la pila cuando se llama a la función (los valores de los parámetros se insertan en la pila antes de llamar a la función y se pasan a la función llamada a través del pila).

2.2 ¿Cómo encuentra la función de formato el valor del parámetro que se va a formatear en la pila y completa el formateo?

       Para las funciones de formato que admiten parámetros variables, no puede determinar cuántos parámetros se pasan realmente a la función de llamada principal. Solo puede leer los datos correspondientes de la pila en secuencia de acuerdo con los caracteres de formato establecidos. Para comprender por qué ocurren problemas (o incluso fallas) durante el formateo, debe comprender cómo la función de formato analiza los valores de los parámetros que se formatearán desde la pila en secuencia en función de los caracteres de formato. Para comprender este problema, también debe comprender cómo se insertan los parámetros en la pila durante las llamadas a funciones mencionadas anteriormente . El orden en que se insertan varios parámetros en la pila está relacionado con la convención de llamada a funciones.

       Tomando la función de formato sprintf como ejemplo, formatea los valores de dos variables de tipo int en cadenas:

int i = 2, j= 3;
char szBuffer[128] = { 0 };
sprintf( szBuffer, "i = %d, j =%d", i, j );

Entre ellos, la declaración de la función de formato sprintf es:

int __cdecl sprintf( char *buffer, const char *format, ... );

Esta función es una función del sistema proporcionada en la biblioteca de tiempo de ejecución de C. Admite parámetros variables y puede formatear múltiples parámetros.

Para funciones con parámetros variables, la convención de llamada es generalmente __cdecl (llamada C) y no se puede declarar como __stdcall, porque para funciones de parámetros variables, solo la persona que llama a la función sabe cuántos parámetros se pasan, y solo la persona que llama a la función lo sabe. sepa cuánta memoria de pila ocupada por los parámetros debe liberarse hasta que vaya allí, por lo que debe declararse como una llamada __cdecl. Para las llamadas __stdcall, es la función llamada la que libera la memoria de pila ocupada por los parámetros entrantes. A través de esta función que admite parámetros variables, puede ayudar a recordar la diferencia entre llamadas __cdecl y llamadas __stdcall. Este es un pequeño truco.

       Además, para la llamada __cdecl, los parámetros se insertan en la pila de derecha a izquierda, es decir, el valor del parámetro j se inserta primero en la pila y luego el valor del parámetro i se inserta en la pila. Para la llamada estándar __stdcall de uso común, los parámetros también se insertan en la pila de derecha a izquierda.

       Anteriormente, hemos visto la implementación del código interno de la función Format que admite el formato de parámetros variables en la clase CString que viene con VC6.0. A partir de estos códigos, podemos ver cómo la función de formato se encuentra en la pila en secuencia de acuerdo con el Formato de caracteres Datos formateados correspondientes.

       La función de formato no importa cuántos parámetros pase, analiza los caracteres de formato establecidos de izquierda a derecha en función de los caracteres de formato establecidos (centrándose principalmente en el tipo y número de caracteres de formato), y luego de acuerdo con los caracteres de formato Ir a la pila para buscar datos formateados. La función de formateo puede obtener la primera dirección de la memoria de la pila ocupada por los parámetros pasados. Todos los parámetros que se pasarán se insertan en la pila en secuencia, por lo que la memoria de la pila ocupada por estos parámetros es continua y cercana.

       ¡Tomemos el ejemplo anterior como ejemplo para explicar cómo la función de formato obtiene los datos que se van a formatear en función de los caracteres de formato ! Primero, el diagrama de distribución de la pila se inserta en la pila antes de dibujar la llamada a la función, como se muestra a continuación:

Cuando se analiza el primer carácter de formato %d , vaya a la memoria de la pila para obtener 4 bytes de memoria y luego lea el valor en la memoria como el contenido que se va a formatear. Después de analizar el primer %d, el puntero p que guarda la primera dirección de la memoria de la pila ocupada por el parámetro se desplaza hacia atrás en 4 bytes ocupados por los datos correspondientes a %d, en preparación para analizar el siguiente parámetro que se formateará.

       Cuando se analiza el segundo %d , vaya a la dirección inicial de la memoria guardada en el puntero p para extraer 4 bytes de memoria y escriba el contenido de estos 4 bytes de memoria en la cadena de destino como los datos a formatear. De manera similar, desplace el puntero p que contiene la primera dirección de la memoria de la pila ocupada por el parámetro hacia atrás en 4 bytes ocupados por los datos correspondientes a %d, para prepararse para analizar el siguiente parámetro que se va a formatear.

2.3 Descripción de escenarios de problemas anormales correspondientes al formateador de cadenas %s

       Si el carácter de formato es %s, que corresponde a una cadena, el contenido del parámetro correspondiente insertado en la pila antes de llamar a la función es la primera dirección de la cadena. De esta manera, cuando se procesa %s, el contenido antes de llamar la función se empuja desde la pila, lee la primera dirección de la cadena y luego lee la cadena en la memoria correspondiente a la primera dirección.

       Si el carácter de formato no coincide con el tipo de parámetro formateado, la primera dirección de la cadena recuperada de la pila según %s es una dirección muy pequeña, como 0x00000001, y luego acceder a esta dirección muy pequeña provocará una infracción de acceso a la memoria. Hemos dicho muchas veces que en los sistemas Windows, el área de memoria con un valor de dirección inferior a 64 KB es un área de memoria de dirección NULL y el acceso está prohibido. Una vez accedido, se activará una violación de acceso a la memoria y el sistema finalizará por la fuerza el proceso. Nos hemos encontrado con este escenario problemático antes.

2.4 Para facilitar la comprensión del mecanismo anterior, se adjunta el código fuente de implementación de la función Formato de la clase CString en VC6.0.

       Hay muchas funciones que admiten el formato de parámetros variables, como printf y sprintf, pero estas funciones de tiempo de ejecución de C no pueden ver directamente su implementación de código fuente interno en Visual Studio.

De hecho, se puede encontrar el código fuente de implementación interna de printf y sprintf . Todos llaman internamente a _output_l . Aunque esta función no se puede ingresar durante la depuración de un solo paso, _output_l se puede encontrar en el archivo output.c, por lo que printf aún se puede encontrado ¡Y la implementación subyacente de sprintf!

Para el archivo output.c , busque directamente con Everything y podrá encontrarlo en la máquina donde instalé Visual Studio 2010:

Anteriormente, extrajimos el código fuente de implementación completo de la clase CString de la biblioteca VC6.0 MFC. Para facilitar que todos comprendan el mecanismo de análisis interno de la función de formato explicado anteriormente, aquí proporcionamos el código relacionado con CString::Format:

// formatting (using wsprintf style formatting)
void CString::Format(LPCTSTR lpszFormat, ...)
{
    assert(IsValidString(lpszFormat));

    va_list argList;
    va_start(argList, lpszFormat);
    FormatV(lpszFormat, argList);
    va_end(argList);
}

#define _INTSIZEOF(n)          ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

#define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
#define __crt_va_arg(ap, t)     (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
#define __crt_va_end(ap)        ((void)(ap = (va_list)0))

#define va_start __crt_va_start
#define va_arg   __crt_va_arg
#define va_end   __crt_va_end
#define va_copy(destination, source) ((destination) = (source))


void CUIString::FormatV(LPCTSTR lpszFormat, va_list argList)
{
    assert(IsValidString(lpszFormat));

    va_list argListSave = argList;

    // make a guess at the maximum length of the resulting string
    int nMaxLen = 0;
    for (LPCTSTR lpsz = lpszFormat; *lpsz != '\0'; lpsz = _tcsinc(lpsz))
    {
        // handle '%' character, but watch out for '%%'
        if (*lpsz != '%' || *(lpsz = _tcsinc(lpsz)) == '%')
        {
            nMaxLen += _tclen(lpsz);
            continue;
        }

        int nItemLen = 0;

        // handle '%' character with format
        int nWidth = 0;
        for (; *lpsz != '\0'; lpsz = _tcsinc(lpsz))
        {
            // check for valid flags
            if (*lpsz == '#')
                nMaxLen += 2;   // for '0x'
            else if (*lpsz == '*')
                nWidth = va_arg(argList, int);
            else if (*lpsz == '-' || *lpsz == '+' || *lpsz == '0' ||
                *lpsz == ' ')
                ;
            else // hit non-flag character
                break;
        }
        // get width and skip it
        if (nWidth == 0)
        {
            // width indicated by
            nWidth = _ttoi(lpsz);
            for (; *lpsz != '\0' && _istdigit(*lpsz); lpsz = _tcsinc(lpsz))
                ;
        }
        assert(nWidth >= 0);

        int nPrecision = 0;
        if (*lpsz == '.')
        {
            // skip past '.' separator (width.precision)
            lpsz = _tcsinc(lpsz);

            // get precision and skip it
            if (*lpsz == '*')
            {
                nPrecision = va_arg(argList, int);
                lpsz = _tcsinc(lpsz);
            }
            else
            {
                nPrecision = _ttoi(lpsz);
                for (; *lpsz != '\0' && _istdigit(*lpsz); lpsz = _tcsinc(lpsz))
                    ;
            }
            assert(nPrecision >= 0);
        }

        // should be on type modifier or specifier
        int nModifier = 0;
        if (_tcsncmp(lpsz, _T("I64"), 3) == 0)
        {
            lpsz += 3;
            nModifier = FORCE_INT64;
#if !defined(_X86_) && !defined(_ALPHA_)
            // __int64 is only available on X86 and ALPHA platforms
            assert(FALSE);
#endif
        }
        else
        {
            switch (*lpsz)
            {
                // modifiers that affect size
            case 'h':
                nModifier = FORCE_ANSI;
                lpsz = _tcsinc(lpsz);
                break;
            case 'l':
                nModifier = FORCE_UNICODE;
                lpsz = _tcsinc(lpsz);
                break;

                // modifiers that do not affect size
            case 'F':
            case 'N':
            case 'L':
                lpsz = _tcsinc(lpsz);
                break;
            }
        }

        // now should be on specifier
        switch (*lpsz | nModifier)
        {
            // single characters
        case 'c':
        case 'C':
            nItemLen = 2;
            va_arg(argList, TCHAR_ARG);
            break;
        case 'c'|FORCE_ANSI:
        case 'C'|FORCE_ANSI:
            nItemLen = 2;
            va_arg(argList, CHAR_ARG);
            break;
        case 'c'|FORCE_UNICODE:
        case 'C'|FORCE_UNICODE:
            nItemLen = 2;
            va_arg(argList, WCHAR_ARG);
            break;

            // strings
        case 's':
            {
                LPCTSTR pstrNextArg = va_arg(argList, LPCTSTR);
                if (pstrNextArg == NULL)
                    nItemLen = 6;  // "(null)"
                else
                {
                    nItemLen = lstrlen(pstrNextArg);
                    nItemLen = max(1, nItemLen);
                }
            }
            break;

        case 'S':
            {
#ifndef _UNICODE
                LPWSTR pstrNextArg = va_arg(argList, LPWSTR);
                if (pstrNextArg == NULL)
                    nItemLen = 6;  // "(null)"
                else
                {
                    nItemLen = wcslen(pstrNextArg);
                    nItemLen = max(1, nItemLen);
                }
#else
                LPCSTR pstrNextArg = va_arg(argList, LPCSTR);
                if (pstrNextArg == NULL)
                    nItemLen = 6; // "(null)"
                else
                {
                    nItemLen = lstrlenA(pstrNextArg);
                    nItemLen = max(1, nItemLen);
                }
#endif
            }
            break;

        case 's'|FORCE_ANSI:
        case 'S'|FORCE_ANSI:
            {
                LPCSTR pstrNextArg = va_arg(argList, LPCSTR);
                if (pstrNextArg == NULL)
                    nItemLen = 6; // "(null)"
                else
                {
                    nItemLen = lstrlenA(pstrNextArg);
                    nItemLen = max(1, nItemLen);
                }
            }
            break;

        case 's'|FORCE_UNICODE:
        case 'S'|FORCE_UNICODE:
            {
                LPWSTR pstrNextArg = va_arg(argList, LPWSTR);
                if (pstrNextArg == NULL)
                    nItemLen = 6; // "(null)"
                else
                {
                    nItemLen = wcslen(pstrNextArg);
                    nItemLen = max(1, nItemLen);
                }
            }
            break;
        }

        // adjust nItemLen for strings
        if (nItemLen != 0)
        {
            if (nPrecision != 0)
                nItemLen = min(nItemLen, nPrecision);
            nItemLen = max(nItemLen, nWidth);
        }
        else
        {
            switch (*lpsz)
            {
                // integers
            case 'd':
            case 'i':
            case 'u':
            case 'x':
            case 'X':
            case 'o':
                if (nModifier & FORCE_INT64)
                    va_arg(argList, __int64);
                else
                    va_arg(argList, int);
                nItemLen = 32;
                nItemLen = max(nItemLen, nWidth+nPrecision);
                break;

            case 'e':
            case 'g':
            case 'G':
                va_arg(argList, DOUBLE_ARG);
                nItemLen = 128;
                nItemLen = max(nItemLen, nWidth+nPrecision);
                break;

            case 'f':
                {
                    double f;
                    LPTSTR pszTemp;

                    // 312 == strlen("-1+(309 zeroes).")
                    // 309 zeroes == max precision of a double
                    // 6 == adjustment in case precision is not specified,
                    //   which means that the precision defaults to 6
                    pszTemp = (LPTSTR)_alloca(max(nWidth, 312+nPrecision+6));

                    f = va_arg(argList, double);
                    _stprintf( pszTemp, _T( "%*.*f" ), nWidth, nPrecision+6, f );
                    nItemLen = _tcslen(pszTemp);
                }
                break;

            case 'p':
                va_arg(argList, void*);
                nItemLen = 32;
                nItemLen = max(nItemLen, nWidth+nPrecision);
                break;

                // no output
            case 'n':
                va_arg(argList, int*);
                break;

            default:
                assert(FALSE);  // unknown formatting option
            }
        }

        // adjust nMaxLen for output nItemLen
        nMaxLen += nItemLen;
    }

    GetBuffer(nMaxLen);
    //VERIFY(_vstprintf(m_pchData, lpszFormat, argListSave) <= GetAllocLength());

    // 将上一句的VERIFY代码注释掉,重新写
    // 此处去掉了VERIFY
    int nWriteCount = GetAllocLength() + 1;
    int nLen = _vstprintf(m_pchData, nWriteCount, lpszFormat, argListSave);
    assert( nLen <= GetAllocLength() );

    ReleaseBuffer();

    va_end(argListSave);
}

El código anterior incluye dos macros: __crt_va_arg y _INTSIZEOF . Estas dos macros son únicas y están implementadas de manera inteligente. Para comprender el código, primero debe comprender el significado de estas dos macros. La macro _INTSIZEOF implementa una alineación de 4 bytes. La macro __crt_va_arg realiza el desplazamiento acumulativo hacia atrás del puntero de dirección de memoria de la pila ap y, al mismo tiempo, devuelve la dirección de memoria de la pila del parámetro actual que se va a formatear.

Algunas personas pueden decir, ¿por qué deberíamos mirar el código en CString en el antiguo VC6.0 y por qué no mirar el código fuente de implementación de la clase CString en la nueva versión de Visual Studio? La clase CString en la nueva versión de Visual Studio se implementa mediante plantillas, lo que parece muy laborioso. El código en VC6.0 parece más intuitivo y fácil de entender. Sirve principalmente para comprender el mecanismo de análisis dentro de la función de formato. Como puedes entender, este mecanismo y proceso de análisis pueden analizar el problema.

2.5 Si desea formatear los datos de un objeto de clase C++ y el objeto contiene varios miembros de datos, debe especificar claramente el miembro de datos que se va a formatear.

       Tome la clase de cadena CStdString en la biblioteca de código abierto duilib como ejemplo. La clase contiene dos miembros de datos, de la siguiente manera:

class DIRECTUI_API CStdString
{
public:
    enum { MAX_LOCAL_STRING_LEN = 16 };

    CStdString();                                    
    CStdString(const TCHAR ch);

    LPCTSTR GetData(){ return m_pstr;}

    // ......  // 其他成员函数省略

protected:
    LPTSTR m_pstr;                           // 字符指针
    TCHAR m_szBuffer[MAX_LOCAL_STRING_LEN];  // 字符缓冲区
};

Al formatear, debe quedar claro qué miembro de datos está formateado y los objetos de clase C++ no se pueden pasar directamente como parámetros a la función. Porque si el parámetro pasado es un objeto de clase, todos los valores de los miembros de datos de toda la clase se enviarán a la pila, pero en realidad solo queremos formatear un determinado miembro de datos en la clase. Los datos provocarán posteriores Hay un problema con el procesamiento de caracteres de formato. Por ejemplo, el código problemático es el siguiente:

CStdString strId;
CStdString strName;

// 中间对strId和strName的赋值代码省略,假设这两个变量中已经存放了真实的字符串数据。
// ...

CStdString strLog;
strLog.Format(_T("strId: %s, strName: %s"), strId, strName);

El método de escritura aquí es problemático. Los objetos de clase strId y strName se pasan directamente como parámetros, y el CStdString correspondiente contiene dos miembros de datos m_pstr y m_szBuffer. De esta manera, ambas variables miembro se insertan en la pila antes de llamar a la función. en la pila. De hecho, lo que queremos formatear es m_pstr. Por supuesto, este lugar es un poco confuso: CStdString es una clase de cadena que procesa cadenas, por supuesto, todos pasan directamente el objeto al formatear.

       El enfoque correcto es llamar a la interfaz CStdString::GetData para obtener la variable miembro m_pstr. Solo necesitamos formatear los datos del miembro m_pstr . El código correcto es el siguiente:

CStdString strId;
CStdString strName;

// 中间对strId和strName的赋值代码省略,假设这两个变量中已经存放了真实的字符串数据。
// ...

CStdString strLog;
strLog.Format(_T("strId: %s, strName: %s"), strId.GetData(), strName.GetData() );

3. Análisis de problemas y resolución de problemas en este caso.

3.1 Código de problema

       El código de registro de impresión problemático es el siguiente:

El primer parámetro es la primera dirección de la cadena, el segundo parámetro es un valor entero, el tercer parámetro y el cuarto parámetro son ambos de tipo bool y el quinto parámetro también es la primera dirección de la cadena. Generalmente, este tipo de problema se debe a la inconsistencia entre el formateador y el tipo de parámetro que se va a formatear, pero no se ha encontrado ningún problema obvio de discrepancia.

       Cuando estaba haciendo cursos de capacitación sobre depuración de software C ++ y solución de excepciones, hablé sobre el problema del formateador que no coincide con los parámetros a formatear. El colega que mantenía este código sospechaba si estaba relacionado con los parámetros insertados en la pila. De hecho, desde el punto de vista del ensamblador, al llamar a una función, para la convención de llamada de C, los parámetros se insertan en la pila de derecha a izquierda y la función llamada lee internamente los valores de los parámetros entrantes de la pila.

       Para formatear funciones que admiten parámetros variables, los datos correspondientes a formatear se recuperan secuencialmente de la pila de acuerdo con el formateador establecido dentro de la función. Si el formateador y el parámetro a formatear son inconsistentes, se producirá una desviación de dirección al leer datos de la pila y se leerá un área de memoria que no debería leerse, lo que provocará una excepción.

3.2 Análisis preliminar

       Al principio sospeché que los parámetros bool 3 y 4 usaban el formateador %d. ¿Hay algún problema? La variable de tipo bool parece enviar solo un byte de datos de memoria a la pila, pero cuando se encuentra el carácter de formato %d dentro de la función de formato, se quitarán 4 bytes de la pila. Esto parece ser inconsistente, por lo que el bool El tipo debe formatearse. Se agregó una conversión forzada de tipo int antes del parámetro. Después de compilar el código y probarlo, el quinto parámetro aún no se puede imprimir.

       De hecho, el parámetro bool que se va a formatear son cuatro bytes insertados al insertarlos en la pila, es decir, cuatro bytes alineados. Así que les pregunté a mis colegas cuál es el segundo tipo de parámetro. Este parámetro es el valor de retorno de una función. Está bien si no pregunto, pero es malo si pregunto. El tipo de parámetro es UINT64, que es un 64 sin firmar. entero de bits. El formato del resultado. El formateador utilizado es %d. Aquí es donde radica el problema. Se debe utilizar el formateador %llu correspondiente al tipo entero sin signo de 64 bits (tenga en cuenta que no utilice %lld aquí porque UINT64 no está firmado) . Utilice %d. Debe haber algún problema.

3.3 ¿Por qué hay problemas al utilizar el formateador %d para datos UINT64?

       Dentro de la función de formateo, de acuerdo con el carácter de formato requerido actualmente, los datos en la memoria correspondientes al número de bytes se leen de la memoria de la pila como los datos actuales que se van a formatear y, al mismo tiempo, la dirección de la memoria de la pila se desplaza hacia atrás. correspondiente al carácter de formato actual.La longitud de la memoria para preparar el procesamiento del formateo de los siguientes datos del formateador. Luego saque el segundo formateador y procese el segundo parámetro a formatear. ¡Etcétera!

       El código de impresión que causó el problema en esta pregunta es el siguiente:

La línea de código que causó el problema anterior pasó parámetros 5. Antes de llamar a la función de impresión, estos 5 parámetros deben insertarse en la pila y pasarse a la función de impresión de registros llamada. El diagrama de distribución de la pila después de insertar estos parámetros en la pila es el siguiente:

En esta pregunta, al formatear los datos UINT64 correspondientes al carácter de formato %d, debido a que el carácter de formato %d corresponde a 4 bytes, solo los datos de 4 bytes en la memoria se recuperan de la memoria de la pila (los datos no son 64- bit).8 bytes correspondientes a los datos enteros) y, al mismo tiempo, el puntero de la memoria de la pila se desplaza hacia atrás en 4 bytes correspondientes al carácter de formato %d. Esto hace que se tomen los datos enteros UINT64 al analizar el tercer %. Carácter de formato D. Parte de él está desalineado, ¡así que algo sale mal! Entonces continúe calculando, al analizar la memoria correspondiente al quinto carácter de formato %s, se toma la cuarta memoria de tipo bool, el valor bool se usa como la primera dirección de una cadena y luego se toman los caracteres en la dirección de lectura Cadena , debido a que la dirección es muy pequeña, debería provocar una infracción de acceso a la memoria y provocar un bloqueo. Sin embargo, no falló durante la operación real y se generó un valor (nulo). ¿Podría ser que se implementó una protección especial en la función de formato y se encontró que la dirección estaba vacía y se generó una cadena nula directamente? ? Supongo que sí.

3.4 Solución

        La solución a este problema es muy sencilla, basta con cambiar el carácter de formato correspondiente al parámetro de tipo UINT64 a %llu. Para resumirlo en una oración, use un formateador con una longitud correspondiente al tipo de parámetro de formato.

4. Finalmente

       Este caso es muy típico. A través de este caso, se explica en detalle el mecanismo interno y el proceso de análisis de la función de formateo. Tiene un valor guía directo y una referencia práctica directa para solucionar problemas de formato de datos (como datos que no se imprimen o la programa falla anormalmente). Al consultar el proceso de análisis de este ejemplo, cuando encuentre problemas de formato más adelante, podrá analizarlos y solucionarlos usted mismo.

Supongo que te gusta

Origin blog.csdn.net/chenlycly/article/details/132549186
Recomendado
Clasificación