-->
当前位置:首页 > 实验 > 正文内容

STL库安全实验

Luz4年前 (2020-12-11)实验2319

实验目的

  • 了解STL标准模板库

  • 了解C++中的STL库安全

  • 掌握安全的C++中的STL库操作

实验原理

1.STL(标准模板库)
  STL是Standard Template Library的简称,中文名标准模板库,惠普实验室开发的一系列软件的统称。它是由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室工作时所开发出来的。从根本上说,STL是一些"容器"的集合,这些"容器"有list,vector,set,map等,STL也是算法和其他一些组件的集合。这里的"容器"和算法的集合指的是世界上很多聪明人很多年的杰作。STL的目的是标准化组件,这样就不用重新开发,可以使用现成的组件。STL现在是C++的一部分,因此不用安装额外的库文件。
  STL的版本很多,常见的有HP STL、PJ STL、 SGI STL等。

2.STL库安全
  STL(Standard Template Library标准模板库)是C++开发中的常用组件,它通过容器+迭代子+算法的灵活组合,可以满足很多应用场景,但使用不好则易留下安全隐患。

实验步骤

步骤1:引用容器前后元素时要确保容器元素存在

  1.1 没有判断是否为空就直接通过引用STL容器首尾元素,这在容器为空时会导致程序异常。
  错误示例:

bool NoCompliant(const NodeKeyList &srcList, const NodeKeyList &snkList)

{

   NodeKey srcNode = srcList.front();

   NodeKey snkNode = snkList.back();

   /* ...do something... */

}

  示例代码对函数的入参srcList没有判断长度直接通过front()和back()方法取了第一个和最后一个元素,在容器列表为空的情况下,会导致程序异常,与front()类似的还有通过begin()数据下标获取对应元素,比如*srcList.begin(),或者srcList.begin()->GetID(),或者是srcList[0](在为vector时)。
  推荐做法:

bool Compliant(const NodeKeyList &srcList, const NodeKeyList &snkList)

{    

   if (srcList.empty() || snkList.empty()) //【修改】确保STL容器内有元素存在

   {

       return false;

   }

   NodeKey srcNode = srcList.front();

   NodeKey snkNode = snkList.back();

   /* ...do something... */

}

步骤2:迭代子使用前必须保证迭代子有效

  2.1 STL算法std::find()、std::find_if()和std::set::find()等有可能返回容器的end()位置,迭代子定义时可以不初始化,或者初始化指向容find()等方法返回的位置,与指针类似地,若未判断迭代子有效性,直接引用迭代子有可能导致程序崩溃。
  错误示例:

void STLIterTest::IterReference_NoCompliant(int CmdCode, MAP_GENKEY_VALUE& allResult)

{    

   TEGenKey tmpKey;

   tmpKey.attrID = DWDMTL1_ATTRPORT_CLIENTPRO;

   tmpKey.objectID = it->first.objectID;

   MAP_GENKEY_VALUE::iterator iter = allResult.find( tmpKey );

   if ("FC100" != iter->second.sValue) //FC100设置为无效

   {

       it->second.access = TEGenVar::ACCESS_INVALID;

   }

}

  示例代码通过map的find函数返回的迭代子iter,if语句直接通过iter->second来引用变量,如果迭代子iter指向为allResult的end()位置,则程序会崩溃。
  推荐做法:

void STLIterTest::IterReference_Compliant(int CmdCode, MAP_GENKEY_VALUE& allResult)

{

   TEGenKey tmpKey;

   tmpKey.attrID = DWDMTL1_ATTRPORT_CLIENTPRO;

   tmpKey.objectID = it->first.objectID;

   MAP_GENKEY_VALUE::iterator iter = allResult.find(tmpKey);

   if (iter != allResult.end()) //【修改】确保迭代子有效后再进行操作

   {

       if ( "FC100" != iter->second.sValue)

       {

           it->second.access = TEGenVar::ACCESS_INVALID;

       }

   }

}

步骤3:必须确保迭代子指向的内容有效
  3.1 在理解上迭代子可以视为C指针,迭代子只有在指向了容器中一个存在的对象时,访问才是安全有效的,其他情况的访问都可能存在风险。典型问题:
  对连续内存容器来说(如std::vector)会分配一块固定内存来保存连续对象,在插入新元素后(成员函数包括:reserve(),resize(), push_back(),insert()等),可能会引起容器重新分配内存和数据迁移,如果在插入元素之前使用迭代子保存了迭代子位置,那么插入新元素之后,前面保存的迭代子就可能是无效的。
  错误示例:下列代码的迭代子在操作过程中失效。

void NoCompliant()

{

   deque<double> d;

   double data[5] = { 2.3, 3.7, 1.4, 0.8, 9.6 };

   deque<double>::iterator pos = d.begin();

   for (size_t i = 0; i < 5; ++i)

   {

       d.insert(pos++, data[i] + 41); //【错误】insert操作后,pos已失效

   }

}

  Insert操作后,迭代子pos已经失效,执行自增操作导致异常。
  推荐做法:

void Compliant()

{

   double data[5] = { 2.3, 3.7, 1.4, 0.8, 9.6 };

   deque<double> d;

   deque<double>::iterator pos = d.begin();

   for (size_t i = 0; i < 5; ++i)

   {

         pos = d.insert(pos, data[i] + 41); /*【修改】通过返回值获得新的有效的迭代子 */

       ++pos;

   }

}

  std::remove和std::remove_if仅会将被删除元素后移并返回应该被删除元素位置的迭代子,并没有正在从容器中删除对象,需要另配合erase函数才能删除,所以一般建议配合一起使用。
  错误示例2:下列代码中错误的仅使用remove()函数来删除容器中元素。

void NoCompliant()

{

   vector<int> container;

   int value = 42;

   iterator end = remove( container.begin(), container.end(), value);

   for (iterator i = container.begin(); i != container.end(); ++i)

   {

       cout << "Container element: " << *i << endl;

   }

}

  remove() 删除任一个成员后返回值将指向任一个成员,值将不可预知。所以被删除后需要立即调用 erase()抹去,防止不可预知的数据访问。
  推荐做法:

void Compliant()

{

   vector<int> container;

   int value = 42;

   container.erase(remove(container.begin(),container.end(),value), container.end()); /*【修改】remove删除成员后立即调用erase,确保迭代子指向的内容是有效的*/

   for (iterator i = container.begin(); i != container.end(); ++i)

   {

       cout << "Container element: " << *i << endl;

   }

}

步骤4:正确处理容器的erase()方法与迭代子的关系
  4.1 调用容器的erase(iter)方法后,迭代子指向的对象被析构,迭代子已经失效,如果再对迭代子执行递增递减或者引用操作会导致程序崩溃。
  错误示例:下列代码中的迭代子在执行删除操作过程中已失效。

void STLIterTest::IterVisitContainer_NoCompliant()

{

   std::map<oid,NE>::iterator it = m_mapID2NE.begin();

   for (; it != m_mapID2NE.end(); )

   {

       if (pNEInfo->GetNEState(ulNEID) == NESTATE_LOGIN)

       {

           m_mapID2NE.erase(iter);

           iter++;  //【错误】erase后,iter指向的对象可能已失效

       }

       else {++iter;}

   }

}

  推荐做法:将迭代子后置递增作为erase()的参数。

void STLIterTest::IterVisitContainer_Compliant()

{

   std::map<oid,NE>::iterator it = m_mapID2NE.begin();

   for (; it != m_mapID2NE.end(); )

   {

       if ( pNEInfo->GetNEState(ulNEID) == NESTATE_LOGIN)

       {

           m_mapID2NE.erase(iter++); //【修改】将迭代子后置递增作为erase参数

       }

       else {++iter;}

   }

}

  也可以使用earse方法的返回值来保存迭代子,因为返回的是被删除元素迭代子指向的下一个元素位置:

iter = erase(iter)

  注意这种用法可以用于list和vector的erase(),但不适用于map。因为std::map::erase()的返回值在不同STL实现版本是有差异的,有的有返回值,有的没有返回值,所以对map只能使用推荐做法。

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。