在snprintf输出中看到的杂散字符

时间:2018-09-04 10:26:02

标签: c arm embedded

我在C中有一个字符串创建函数,该函数接受array of structs作为其参数,并根据预定义的格式(例如python中的列表列表)输出字符串。
这是功能

typedef struct
{
    PacketInfo_t PacketInfo;
    char Gnss60[1900]; 
    //and other stuff...
} Track_json_t;

typedef struct 
{
    double latitude;
    double longitude;
} GPSPoint_t;

typedef struct
{
    UInt16          GPS_StatusCode;
    UInt32          fixtime;
    GPSPoint_t      point;
    double          altitude;
    unsigned char GPS_Satilite_Num;
} GPS_periodic_t;

unsigned short SendTrack()
{
    Track_json_t i_sTrack_S;
    memset(&i_sTrack_S, 0x00, sizeof(Track_json_t));
    getEvent_Track(&i_sTrack_S);
    //Many other stuff added to the i_sTrack_S struct...
    //Make a JSON format out of it
    BuildTrackPacket_json(&i_sTrack_S, XPORT_MODE_GPRS);
}

Track_json_t *getEvent_Track(Track_json_t *trk)
{
    GPS_periodic_t l_gps_60Sec[60];
    memset(&l_gps_60Sec, 0x00,
           sizeof(GPS_periodic_t) * GPS_PERIODIC_ARRAY_SIZE);
    getLastMinGPSdata(l_gps_60Sec, o_gps_base);
    get_gps60secString(l_gps_60Sec, trk->Gnss60);
    return trk;
}

void get_gps60secString(GPS_periodic_t input[60], char *output)
{
    int i = 0;
    memcpy(output, "[", 1); ///< Copy the first char as [
    char temp[31];
    for (i = 0; i < 59; i++) { //Run for n-1 elements
        memset(temp, 0, sizeof(temp));
        snprintf(temp, sizeof(temp), "[%0.8f,%0.8f],",
            input[i].point.latitude, input[i].point.longitude);
        strncat(output, temp, sizeof(temp));
    }
    memset(temp, 0, sizeof(temp)); //assign last element
    snprintf(temp, sizeof(temp), "[%0.8f,%0.8f]]",
             input[i].point.latitude, input[i].point.longitude);
    strncat(output, temp, sizeof(temp));
}

因此,函数的输出必须为格式字符串

  

[[12.12345678,12.12345678],[12.12345678,12.12345678],...]

但是有时候我会得到一个看起来像

的字符串
  

[[[12.12345678,12.12345678], [55.01 ] [12.12345678,12.12345678],...]
  [[21.28211567,84.13454083], [21.28211533,21.22 [21.28211517,84.13454000],..]

以前,我在函数get_gps60secString上发生了缓冲区溢出,我通过使用snprintfstrncat来解决了这个问题。

注意:这是一个嵌入式应用程序,该错误每天发生一次或两次(共1440个数据包)

问题
1.这可能是由snprintf / strncat进程中的中断引起的吗?
2.这可能是由内存泄漏,覆盖堆栈或其他原因导致的分段问题引起的吗?
基本上,我想了解是什么原因导致了字符串损坏。

很难找到原因并修复此错误。


编辑:
我使用了chux's函数。以下是最小,完整和可验证的示例

/*
 * Test code for SO question https://stackoverflow.com/questions/5216413
 * A Minimal, Complete, and Verifiable Example
 */

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <stdbool.h>
#include <signal.h>
#include <unistd.h>

typedef unsigned short UInt16;
typedef unsigned long  UInt32;

#define GPS_PERIODIC_ARRAY_SIZE  60
#define GPS_STRING_SIZE          1900

/* ---------------------- Data Structs --------------------------*/
typedef struct
{
    char Gnss60[GPS_STRING_SIZE];
} Track_json_t;

typedef struct
{
    double          latitude;
    double          longitude;
} GPSPoint_t;

typedef struct
{
    UInt16          GPS_StatusCode;
    UInt32          fixtime;
    GPSPoint_t      point;
    double          altitude;
    unsigned char GPS_Satilite_Num;
} GPS_periodic_t;

/* ----------------------- Global --------------------------------*/
FILE *fptr; //Global file pointer
int res = 0;
int g_last = 0;
GPS_periodic_t l_gps_60Sec[GPS_PERIODIC_ARRAY_SIZE];

/* ----------------------- Function defs --------------------------*/

/* At signal interrupt this function is called.
 * Flush and close the file. And safly exit the program */
void userSignalInterrupt()
{
    fflush(fptr);
    fclose(fptr);
    res = 1;
    exit(0);
}

/* @brief From the array of GPS structs we create a string of the format
 * [[lat,long],[lat,long],..]
 * @param   input   The input array of GPS structs
 * @param   output  The output string which will contain lat, long
 * @param   sz      Size left in the output buffer
 * @return  0       Successfully completed operation
 *          1       Failed / Error
 */
int get_gps60secString(GPS_periodic_t input[GPS_PERIODIC_ARRAY_SIZE], 
                       char *output, size_t sz) 
{
    int cnt = snprintf(output, sz, "[");
    if (cnt < 0 || cnt >= sz)
        return 1;
    output += cnt;
    sz -= cnt;

    int i = 0;
    for (i = 0; i < GPS_PERIODIC_ARRAY_SIZE; i++) {
        cnt = snprintf(output, sz, "[%0.8f,%0.8f]%s", 
                input[i].point.latitude, input[i].point.longitude, 
                i + 1 == GPS_PERIODIC_ARRAY_SIZE ? "" : ",");
        if (cnt < 0 || cnt >= sz)
            return 1;
        output += cnt;
        sz -= cnt;
    }

    cnt = snprintf(output, sz, "]");
    if (cnt < 0 || cnt >= sz)
        return 1;
    return 0; // no error
}

/* @brief   Create a GPS struct with data for testing. It will populate the
 * point field of GPS_periodic_t. Lat starts from 0.0 and increases by 1*10^(-8)
 * and Long will dstart at 99.99999999 and dec by 1*10^(-8)
 *
 * @param   o_gps_60sec Output array of GPS structs
 */
void getLastMinGPSdata(GPS_periodic_t *o_gps_60sec)
{
    //Fill in GPS related data here
    int i = 0;
    double latitude = o_gps_60sec[0].point.latitude;
    double longitude = o_gps_60sec[0].point.longitude;
    for (i = 0; i < 60; i++)
    {
        o_gps_60sec[i].point.latitude = latitude +  (0.00000001 * (float)g_last + 
                                        0.00000001 * (float)i);
        o_gps_60sec[i].point.longitude = longitude -  (0.00000001 * (float)g_last + 
                                        0.00000001 * (float)i);
    }
    g_last = 60;
}

/* @brief   Get the GPS data and convert it into a string
 * @param   trk Track structure with GPS string
 */
int getEvent_Track(Track_json_t *trk)
{
    getLastMinGPSdata(l_gps_60Sec);
    get_gps60secString(l_gps_60Sec, trk->Gnss60, GPS_STRING_SIZE);

    return 0;
}

int main()
{
    fptr = fopen("gpsAno.txt", "a");
    if (fptr == NULL) {
        printf("Error!!\n");
        exit(1);
    }

    //Quit at signal interrupt
    signal(SIGINT, userSignalInterrupt);

    Track_json_t trk;
    memset(&l_gps_60Sec, 0x00, sizeof(GPS_periodic_t) * GPS_PERIODIC_ARRAY_SIZE);

    //Init Points to be zero and 99.99999999
    int i = 0;
    for (i = 0; i < 60; i++) {
        l_gps_60Sec[i].point.latitude =  00.00000000;
        l_gps_60Sec[i].point.longitude = 99.99999999;
    }

    do {
        memset(&trk, 0, sizeof(Track_json_t));
        getEvent_Track(&trk);

        //Write to file
        fprintf(fptr, "%s", trk.Gnss60);
        fflush(fptr);
        sleep(1);
    } while (res == 0);

    //close and exit
    fclose(fptr);
    return  0;
}

注意:以上代码未重新创建错误。
因为这没有strcat陷阱。 我在嵌入式应用程序中测试了此功能。 通过此操作,我发现snprintf返回错误,并且创建的字符串最终为:

  

[17.42401750,78.46098717],[17.42402083, 53.62

它到此结束(由于return 1)。

这是否意味着传递到snprints的数据已损坏?这是一个浮点值。它怎么会被损坏?

解决方案
自从我将sprintf函数更改为不直接处理64位数据的函数以来,从未发现该错误。

这里是功能modp_dtoa2

/** \brief convert a floating point number to char buffer with a
 *         variable-precision format, and no trailing zeros
 *
 * This is similar to "%.[0-9]f" in the printf style, except it will
 * NOT include trailing zeros after the decimal point.  This type
 * of format oddly does not exists with printf.
 *
 * If the input value is greater than 1<<31, then the output format
 * will be switched exponential format.
 *
 * \param[in] value
 * \param[out] buf  The allocated output buffer.  Should be 32 chars or more.
 * \param[in] precision  Number of digits to the right of the decimal point.
 *    Can only be 0-9.
 */
void modp_dtoa2(double value, char* str, int prec)
{
    /* if input is larger than thres_max, revert to exponential */
    const double thres_max = (double)(0x7FFFFFFF);
    int count;
    double diff = 0.0;
    char* wstr = str;
    int neg= 0;
    int whole;
    double tmp;
    uint32_t frac;

    /* Hacky test for NaN
     * under -fast-math this won't work, but then you also won't
     * have correct nan values anyways.  The alternative is
     * to link with libmath (bad) or hack IEEE double bits (bad)
     */
    if (! (value == value)) {
        str[0] = 'n'; str[1] = 'a'; str[2] = 'n'; str[3] = '\0';
        return;
    }

    if (prec < 0) {
        prec = 0;
    } else if (prec > 9) {
        /* precision of >= 10 can lead to overflow errors */
        prec = 9;
    }

    /* we'll work in positive values and deal with the
       negative sign issue later */
    if (value < 0) {
        neg = 1;
        value = -value;
    }


    whole = (int) value;
    tmp = (value - whole) * pow10[prec];
    frac = (uint32_t)(tmp);
    diff = tmp - frac;

    if (diff > 0.5) {
        ++frac;
        /* handle rollover, e.g.  case 0.99 with prec 1 is 1.0  */
        if (frac >= pow10[prec]) {
            frac = 0;
            ++whole;
        }
    } else if (diff == 0.5 && ((frac == 0) || (frac & 1))) {
        /* if halfway, round up if odd, OR
           if last digit is 0.  That last part is strange */
        ++frac;
    }

    /* for very large numbers switch back to native sprintf for exponentials.
       anyone want to write code to replace this? */
    /*
      normal printf behavior is to print EVERY whole number digit
      which can be 100s of characters overflowing your buffers == bad
    */
    if (value > thres_max) {
        sprintf(str, "%e", neg ? -value : value);
        return;
    }

    if (prec == 0) {
        diff = value - whole;
        if (diff > 0.5) {
            /* greater than 0.5, round up, e.g. 1.6 -> 2 */
            ++whole;
        } else if (diff == 0.5 && (whole & 1)) {
            /* exactly 0.5 and ODD, then round up */
            /* 1.5 -> 2, but 2.5 -> 2 */
            ++whole;
        }

        //vvvvvvvvvvvvvvvvvvv  Diff from modp_dto2
    } else if (frac) {
        count = prec;
        // now do fractional part, as an unsigned number
        // we know it is not 0 but we can have leading zeros, these
        // should be removed
        while (!(frac % 10)) {
            --count;
            frac /= 10;
        }
        //^^^^^^^^^^^^^^^^^^^  Diff from modp_dto2

        // now do fractional part, as an unsigned number
        do {
            --count;
            *wstr++ = (char)(48 + (frac % 10));
        } while (frac /= 10);
        // add extra 0s
        while (count-- > 0) *wstr++ = '0';
        // add decimal
        *wstr++ = '.';
    }

    // do whole part
    // Take care of sign
    // Conversion. Number is reversed.
    do *wstr++ = (char)(48 + (whole % 10)); while (whole /= 10);
    if (neg) {
        *wstr++ = '-';
    }
    *wstr='\0';
    strreverse(str, wstr-1);
}

2 个答案:

答案 0 :(得分:4)

这是我关于C语言中安全字符串处理的毫无疑问的指南的一部分(通常是)。通常,我将促进动态内存分配,而不是固定长度的字符串,但是在这种情况下,我假设在嵌入式环境中有问题的。 (尽管应该始终检查类似的假设。)

所以,首先要做的是

  1. 必须明确告知任何在缓冲区中创建字符串的函数。这是不能商量的。

    很明显,除非缓冲区知道缓冲区的结束位置,否则填充缓冲区的函数无法检查缓冲区是否溢出。 “希望缓冲区足够长”不是可行的策略。如果每个人都仔细阅读了文档(不是这样)并且所需的长度从不改变(会的话),那么“记录所需的缓冲区长度”就可以了。剩下的只有一个额外的参数,该参数应为size_t类型(因为这是C库函数中需要长度的缓冲区长度的类型)。

  2. 忘记存在strncpystrncat。还要忘掉strcat。他们不是您的朋友。

    strncpy设计用于特定的用例:确保初始化整个固定长度的缓冲区。它不是为普通字符串设计的,并且由于不能保证输出为NUL终止,因此不会产生字符串。

    如果您要进行NUL终止,则最好使用memmovememcpy,如果您知道源和目标不重叠,则几乎总是案子。由于您希望memmove在短字符串的字符串末尾停止(strncpy不会 这样做),因此请首先使用{{1 }}:strnlen的长度最大,这正是您要移动最大字符数时想要的长度。

    示例代码:

    strnlen

    /* Safely copy src to dst where dst has capacity dstlen. */ if (dstlen) { /* Adjust to_move will have maximum value dstlen - 1 */ size_t to_move = strnlen(src, dstlen - 1); /* copy the characters */ memmove(dst, src, to_move); /* NUL-terminate the string */ dst[to_move] = 0; } 的语义稍显合理,但实际上几乎没有用,因为要使用它,您已经必须知道可以复制多少个字节。为了知道这一点,实际上,您需要知道输出缓冲区中还有多少空间,并且需要知道副本将在输出缓冲区中的何处开始。 [注1]。但是,如果您已经知道复制将从何处开始,那么从头开始搜索缓冲区以找到复制点有什么意义?而且,如果您让strncat进行搜索,那么您如何确定先前计算的起点正确?

    在上面的代码片段中,我们已经计算出副本的长度。我们可以扩展它以执行附加操作而无需重新扫描:

    strncat

    创建字符串之后,可能是我们想要/* Safely copy src1 and then src2 to dst where dst has capacity dstlen. */ /* Assumes that src1 and src2 are not contained in dst. */ if (dstlen) { /* Adjust to_move will have maximum value dstlen - 1 */ size_t to_move = strnlen(src1, dstlen - 1); /* Copy the characters from src1 */ memcpy(dst, src1, to_move); /* Adjust the output pointer and length */ dst += to_move; dstlen -= to_move; /* Now safely copy src2 to just after src1. */ to_move = strnlen(src2, dstlen - 1); memcpy(dst, src2, to_move); /* NUL-terminate the string */ dst[to_move] = 0; } dst的原始值,也可能是我们想知道我们插入dstlen的字节数在所有。在这种情况下,我们可能希望在进行复制之前先复制这些变量,然后保存累积的移动总数。

    以上假设我们从一个空的输出缓冲区开始,但事实并非如此。由于我们仍然需要知道副本将从何处开始,以便知道可以在末尾添加多少个字符,因此我们仍然可以使用dst;我们只需要先扫描输出缓冲区以找到复制点即可。 (只有在没有其他选择的情况下,才执行此操作。循环进行此操作而不是记录下一个复制点是Shlemiel the Painter's algorithm。)

    memcpy
  3. 拥抱/* Safely append src to dst where dst has capacity dstlen and starts * with a string of unknown length. */ if (dstlen) { /* The following code will "work" even if the existing string * is not correctly NUL-terminated; the code will not copy anything * from src, but it will put a NUL terminator at the end of the * output buffer. */ /* Figure out where the existing string ends. */ size_t prefixlen = strnlen(dst, dstlen - 1); /* Update dst and dstlen */ dst += prefixlen; dstlen -= prefixlen; /* Proceed with the append, as above. */ size_t to_move = strnlen(src, dstlen - 1); memmove(dst, src, to_move); dst[to_move] = 0; } 。真的是你的朋友但是请务必检查其返回值。

    如上所述,使用snprintf有点尴尬。它要求您手动检查缓冲区的长度是否不为零(否则,减去1将会是灾难性的,因为该长度是无符号的),并且它要求您手动NUL终止输出缓冲区,这很容易忘记,并且是许多缓冲区的来源错误。这是非常有效的,但是有时需要牺牲一点效率,以便您的代码更易于编写,阅读和验证。

    然后直接将我们引向memmove。例如,您可以替换:

    snprintf

    简单得多

    if (dstlen) {
      size_t to_move = strnlen(src, dstlen - 1);
      memcpy(dst, src, to_move);
      dst[to_move] = 0;
    }
    

    这一切都做得到:检查int copylen = snprintf(dst, dstlen, "%s", src); 是否不为0;仅复制dstlen中适合src的字符,并正确NUL终止dst(除非dst为0)。而且成本极低;解析格式字符串dstlen所需的时间很少,大多数实现已针对这种情况进行了优化。 [注2]

    但是"%s"不是万能药。仍然有一些非常重要的警告。

    首先,snprintf的文档明确指出,不允许任何输入参数重叠输出范围。 (因此它代替了snprintf而不是memcpy。)请记住,重叠包括NUL终止符,因此以下代码尝试将memmove中的字符串加倍,从而导致 Undefined Behavior

    str

    第二次调用char str[BUFLEN]; /* Put something into str */ get_some_data(str, BUFLEN); /* DO NOT DO THIS: input overlaps output */ int result = snprintf(str, BUFLEN, "%s%s", str, str); /* DO NOT DO THIS EITHER; IT IS STILL UB */ size_t len = strnlen(str, cap - 1); int result = snprintf(str + len, cap - len, "%s", str); 的问题在于,终止snprintf的NUL恰好位于输出缓冲区的第一个字节str处。这是重叠的,因此是非法的。

    关于str + len的第二个重要说明是它返回一个值,该值不能忽略。返回的值不是snprintf创建的字符串的长度。这是如果不将字符串截断以适合输出缓冲区的长度。

    如果没有截断发生,那么结果就是结果的长度,必须严格小于输出缓冲区的大小(因为必须有空间容纳NUL终止符,即不认为是结果长度的一部分。)您可以使用此事实来检查是否发生了截断:

    snprintf

    例如,可以使用它来使用较大的动态分配的缓冲区(大小为if (result >= dstlen) /* Output was truncated */ ;不要忘记需要NUL终止)来重做snprintf

    但是请记住,结果是result + 1,即带符号的值。这意味着int无法应付很长的字符串。在嵌入式代码中这不太可能成为问题,但是在可以想到字符串超过2GB的系统上,您可能无法安全地使用snprintf中的%s格式。这也意味着允许snprintf返回负值以指示错误。 snprintf的很旧的实现返回-1表示截断,或者响应于缓冲区长度为0的调用。根据C99(不是Posix的最新版本),这不是标准行为,但是您应该为此做好准备。

    如果缓冲区长度参数太大而不能容纳(带符号的)snprintf,则snprintf的符合标准的实现将返回负值;对于我来说,如果缓冲区长度确定可以,但是对于int来说,未截断的长度太大,预期返回的值是多少呢?如果您使用的转换导致编码错误,则也会返回负值;例如,int转换,其对应的参数包含一个不能转换为多字节(通常为UTF-8)序列的整数。

    简而言之,您应该始终检查%lc的返回值(如果您不这样做,则最新的gcc / glibc版本将产生警告),并且应该准备使其为负数。


因此,我们已经编写了所有产生坐标对的字符串的函数:

snprintf

注释

  1. 您经常会看到这样的代码:

    /* Arguments:
     *    buf      the output buffer.
     *    buflen   the capacity of buf (including room for trailing NUL).
     *    points   a vector of struct Point pairs.
     *    npoints  the number of objects in points.
     * Description:
     *    buf is overwritten with a comma-separated list of points enclosed in
     *    square brackets. Each point is output as a comma-separated pair of
     *    decimal floating point numbers enclosed in square brackets. No more
     *    than buflen - 1 characters are written. Unless buflen is 0, a NUL is
     *    written following the (possibly-truncated) output.
     * Return value:
     *    If the output buffer contains the full output, the number of characters
     *    written to the output buffer, not including the NUL terminator.
     *    If the output was truncated, (size_t)(-1) is returned.
     */
     size_t sprint_points(char* buf, size_t buflen,
                          struct Point const* points, size_t npoints)
     { 
       if (buflen == 0) return (size_t)(-1);
       size_t avail = buflen;
       char delim = '['
       while (npoints) {
         int res = snprintf(buf, avail, "%c[%f,%f]",
                            delim, points->lat, points->lon);
         if (res < 0 || res >= avail) return (size_t)(-1);
         buf += res; avail -= res;
         ++points; --npoints;
         delim = ',';
      }
      if (avail <= 1) return (size_t)(-1);
      strcpy(buf, "]");
      return buflen - (avail - 1);
    }
    

    告诉strncat(dst, src, sizeof(src)); /* NEVER EVER DO THIS! */ 不要在strncat中追加超出src不能容纳的字符显然是没有意义的(除非src没有正确地以NUL终止,在这种情况下,您有更大的问题)。更重要的是,它不会做任何绝对的事情来保护您免受输出缓冲区末尾的影响,因为您没有做任何事情来检查src是否有足够的空间容纳所有这些字符。因此,它所做的一切就是摆脱关于dst不安全的编译器警告。由于此代码与strcat一样完全不安全,因此警告可能会更好。

  2. 您甚至可能会找到一个编译器,它了解strcat足以在编译时解析格式字符串,因此带来的便利是完全免费的。 (而且,如果您当前的编译器不这样做,那么无疑将是将来的版本。)与使用snprintf系列一样,您应该从不尝试通过以下方式来节省键击: 省略格式字符串(用*printf代替snprintf(dst, dstlen, src)。)不安全(如果snprintf(dst, dstlen, "%s", src)包含不重复的src,则行为不确定)。而且慢得多,因为库函数必须解析要复制的整个字符串以寻找百分号,而不仅仅是将其复制到输出中。

答案 1 :(得分:1)

代码使用的函数期望指向 string 的指针,但并不总是将指向 strings 的指针作为参数传递。

  

在snprintf输出中看到的流浪字符

字符串必须具有终止的空字符

strncat(char *, ....期望第一个参数是指向字符串的指针。 memcpy(output, "[",1);不能保证这一点。 @Jeremy

memcpy(output, "[",1);
...
strncat(output, temp,sizeof(temp));

这是流浪字符的候选来源。


strncat(...., ..., size_t size).本身是一个问题,因为size是可用于连接的空间量(减去空字符)。 char * output可用的大小未传递。@Jonathan Leffler。这里strcat()也可以。

相反,请传递output可用的大小以防止缓冲区溢出。

#define N 60

int get_gps60secString(GPS_periodic_t input[N], char *output, size_t sz) {
  int cnt = snprintf(output, sz, "[");
  if (cnt < 0 || cnt >= sz)
    return 1;
  output += cnt;
  sz -= cnt;

  int i = 0;
  for (i = 0; i < N; i++) {
    cnt = snprintf(output, size, "[%0.8f,%0.8f]%s", input[i].point.latitude,
        input[i].point.longitude, i + 1 == N ? "" : ",");
    if (cnt < 0 || cnt >= sz)
      return 1;
    output += cnt;
    sz -= cnt;
  }

  cnt = snprintf(output, sz, "]");
  if (cnt < 0 || cnt >= sz)
    return 1;
  return 0; // no error
}

OP发布了更多代码-将进行审查。

显然,缓冲区char *outputget_gps60secString()之前预先填充了0,因此memcpy(output, "[",1);中丢失的 null字符应该不会引起问题-hmmmmmm < / p>

unsigned short SendTrack()不返回值。 1)使用其结果值为UB。 2)启用所有编译器警告。