svf-tools 1.0.973 → 1.0.975
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/svf/include/AE/Core/AbstractState.h +18 -11
- package/svf/include/AE/Svfexe/AbstractInterpretation.h +46 -33
- package/svf/include/AE/Svfexe/BufOverflowChecker.h +17 -6
- package/svf/include/AE/Svfexe/ICFGSimplification.h +0 -1
- package/svf/include/Graphs/WTO.h +4 -4
- package/svf/lib/AE/Core/AbstractState.cpp +309 -54
- package/svf/lib/AE/Svfexe/AbstractInterpretation.cpp +759 -247
- package/svf/lib/AE/Svfexe/BufOverflowChecker.cpp +27 -37
- package/svf/lib/AE/Svfexe/ICFGSimplification.cpp +1 -0
- package/svf-llvm/tools/AE/ae.cpp +15 -1
- package/svf-llvm/tools/Example/svf-ex.cpp +1 -66
- package/svf/include/AE/Svfexe/SVFIR2AbsState.h +0 -191
- package/svf/lib/AE/Svfexe/SVFIR2AbsState.cpp +0 -957
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
#include "AE/Core/AbstractState.h"
|
|
31
31
|
#include "Util/SVFUtil.h"
|
|
32
|
+
#include "Util/Options.h"
|
|
32
33
|
|
|
33
34
|
using namespace SVF;
|
|
34
35
|
using namespace SVFUtil;
|
|
@@ -97,25 +98,6 @@ AbstractState AbstractState::narrowing(const AbstractState& other)
|
|
|
97
98
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
/// domain widen with other, important! other widen this.
|
|
101
|
-
void AbstractState::widenWith(const AbstractState& other)
|
|
102
|
-
{
|
|
103
|
-
for (auto it = _varToAbsVal.begin(); it != _varToAbsVal.end(); ++it)
|
|
104
|
-
{
|
|
105
|
-
auto key = it->first;
|
|
106
|
-
if (other.getVarToVal().find(key) != other.getVarToVal().end())
|
|
107
|
-
if (it->second.isInterval() && other._varToAbsVal.at(key).isInterval())
|
|
108
|
-
it->second.getInterval().widen_with(other._varToAbsVal.at(key).getInterval());
|
|
109
|
-
}
|
|
110
|
-
for (auto it = _addrToAbsVal.begin(); it != _addrToAbsVal.end(); ++it)
|
|
111
|
-
{
|
|
112
|
-
auto key = it->first;
|
|
113
|
-
if (other._addrToAbsVal.find(key) != other._addrToAbsVal.end())
|
|
114
|
-
if (it->second.isInterval() && other._varToAbsVal.at(key).isInterval())
|
|
115
|
-
it->second.getInterval().widen_with(other._addrToAbsVal.at(key).getInterval());
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
101
|
/// domain join with other, important! other widen this.
|
|
120
102
|
void AbstractState::joinWith(const AbstractState& other)
|
|
121
103
|
{
|
|
@@ -147,27 +129,6 @@ void AbstractState::joinWith(const AbstractState& other)
|
|
|
147
129
|
}
|
|
148
130
|
}
|
|
149
131
|
|
|
150
|
-
/// domain narrow with other, important! other widen this.
|
|
151
|
-
void AbstractState::narrowWith(const AbstractState& other)
|
|
152
|
-
{
|
|
153
|
-
for (auto it = _varToAbsVal.begin(); it != _varToAbsVal.end(); ++it)
|
|
154
|
-
{
|
|
155
|
-
auto key = it->first;
|
|
156
|
-
auto oit = other.getVarToVal().find(key);
|
|
157
|
-
if (oit != other.getVarToVal().end())
|
|
158
|
-
if (it->second.isInterval() && oit->second.isInterval())
|
|
159
|
-
it->second.getInterval().narrow_with(oit->second.getInterval());
|
|
160
|
-
}
|
|
161
|
-
for (auto it = _addrToAbsVal.begin(); it != _addrToAbsVal.end(); ++it)
|
|
162
|
-
{
|
|
163
|
-
auto key = it->first;
|
|
164
|
-
auto oit = other._addrToAbsVal.find(key);
|
|
165
|
-
if (oit != other._addrToAbsVal.end())
|
|
166
|
-
if (it->second.isInterval() && oit->second.isInterval())
|
|
167
|
-
it->second.getInterval().narrow_with(oit->second.getInterval());
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
132
|
/// domain meet with other, important! other widen this.
|
|
172
133
|
void AbstractState::meetWith(const AbstractState& other)
|
|
173
134
|
{
|
|
@@ -191,26 +152,320 @@ void AbstractState::meetWith(const AbstractState& other)
|
|
|
191
152
|
}
|
|
192
153
|
}
|
|
193
154
|
|
|
194
|
-
|
|
195
|
-
|
|
155
|
+
// getGepObjAddrs
|
|
156
|
+
AddressValue AbstractState::getGepObjAddrs(u32_t pointer, IntervalValue offset)
|
|
157
|
+
{
|
|
158
|
+
AddressValue gepAddrs;
|
|
159
|
+
APOffset lb = offset.lb().getIntNumeral() < Options::MaxFieldLimit() ? offset.lb().getIntNumeral()
|
|
160
|
+
: Options::MaxFieldLimit();
|
|
161
|
+
APOffset ub = offset.ub().getIntNumeral() < Options::MaxFieldLimit() ? offset.ub().getIntNumeral()
|
|
162
|
+
: Options::MaxFieldLimit();
|
|
163
|
+
for (APOffset i = lb; i <= ub; i++)
|
|
164
|
+
{
|
|
165
|
+
AbstractValue addrs = (*this)[pointer];
|
|
166
|
+
for (const auto& addr : addrs.getAddrs())
|
|
167
|
+
{
|
|
168
|
+
s64_t baseObj = AbstractState::getInternalID(addr);
|
|
169
|
+
assert(SVFUtil::isa<ObjVar>(PAG::getPAG()->getGNode(baseObj)) && "Fail to get the base object address!");
|
|
170
|
+
NodeID gepObj = PAG::getPAG()->getGepObjVar(baseObj, i);
|
|
171
|
+
(*this)[gepObj] = AddressValue(AbstractState::getVirtualMemAddress(gepObj));
|
|
172
|
+
gepAddrs.insert(AbstractState::getVirtualMemAddress(gepObj));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return gepAddrs;
|
|
177
|
+
}
|
|
178
|
+
// initObjVar
|
|
179
|
+
void AbstractState::initObjVar(ObjVar* objVar)
|
|
180
|
+
{
|
|
181
|
+
NodeID varId = objVar->getId();
|
|
182
|
+
|
|
183
|
+
// Check if the object variable has an associated value
|
|
184
|
+
if (objVar->hasValue())
|
|
185
|
+
{
|
|
186
|
+
const MemObj* obj = objVar->getMemObj();
|
|
187
|
+
|
|
188
|
+
// Handle constant data, arrays, and structures
|
|
189
|
+
if (obj->isConstDataOrConstGlobal() || obj->isConstantArray() || obj->isConstantStruct())
|
|
190
|
+
{
|
|
191
|
+
if (const SVFConstantInt* consInt = SVFUtil::dyn_cast<SVFConstantInt>(obj->getValue()))
|
|
192
|
+
{
|
|
193
|
+
s64_t numeral = consInt->getSExtValue();
|
|
194
|
+
(*this)[varId] = IntervalValue(numeral, numeral);
|
|
195
|
+
}
|
|
196
|
+
else if (const SVFConstantFP* consFP = SVFUtil::dyn_cast<SVFConstantFP>(obj->getValue()))
|
|
197
|
+
{
|
|
198
|
+
(*this)[varId] = IntervalValue(consFP->getFPValue(), consFP->getFPValue());
|
|
199
|
+
}
|
|
200
|
+
else if (SVFUtil::isa<SVFConstantNullPtr>(obj->getValue()))
|
|
201
|
+
{
|
|
202
|
+
(*this)[varId] = IntervalValue(0, 0);
|
|
203
|
+
}
|
|
204
|
+
else if (SVFUtil::isa<SVFGlobalValue>(obj->getValue()))
|
|
205
|
+
{
|
|
206
|
+
(*this)[varId] = AddressValue(AbstractState::getVirtualMemAddress(varId));
|
|
207
|
+
}
|
|
208
|
+
else if (obj->isConstantArray() || obj->isConstantStruct())
|
|
209
|
+
{
|
|
210
|
+
(*this)[varId] = IntervalValue::top();
|
|
211
|
+
}
|
|
212
|
+
else
|
|
213
|
+
{
|
|
214
|
+
(*this)[varId] = IntervalValue::top();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Handle non-constant memory objects
|
|
218
|
+
else
|
|
219
|
+
{
|
|
220
|
+
(*this)[varId] = AddressValue(AbstractState::getVirtualMemAddress(varId));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// If the object variable does not have an associated value, set it to a virtual memory address
|
|
224
|
+
else
|
|
225
|
+
{
|
|
226
|
+
(*this)[varId] = AddressValue(AbstractState::getVirtualMemAddress(varId));
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// getElementIndex
|
|
232
|
+
IntervalValue AbstractState::getElementIndex(const GepStmt* gep)
|
|
233
|
+
{
|
|
234
|
+
// If the GEP statement has a constant offset, return it directly as the interval value
|
|
235
|
+
if (gep->isConstantOffset())
|
|
236
|
+
return IntervalValue((s64_t)gep->accumulateConstantOffset());
|
|
237
|
+
|
|
238
|
+
IntervalValue res(0);
|
|
239
|
+
// Iterate over the list of offset variable and type pairs in reverse order
|
|
240
|
+
for (int i = gep->getOffsetVarAndGepTypePairVec().size() - 1; i >= 0; i--)
|
|
241
|
+
{
|
|
242
|
+
AccessPath::IdxOperandPair IdxVarAndType = gep->getOffsetVarAndGepTypePairVec()[i];
|
|
243
|
+
const SVFValue* value = gep->getOffsetVarAndGepTypePairVec()[i].first->getValue();
|
|
244
|
+
const SVFType* type = IdxVarAndType.second;
|
|
245
|
+
|
|
246
|
+
// Variables to store the lower and upper bounds of the index value
|
|
247
|
+
s64_t idxLb;
|
|
248
|
+
s64_t idxUb;
|
|
249
|
+
|
|
250
|
+
// Determine the lower and upper bounds based on whether the value is a constant
|
|
251
|
+
if (const SVFConstantInt* constInt = SVFUtil::dyn_cast<SVFConstantInt>(value))
|
|
252
|
+
idxLb = idxUb = constInt->getSExtValue();
|
|
253
|
+
else
|
|
254
|
+
{
|
|
255
|
+
IntervalValue idxItv = (*this)[PAG::getPAG()->getValueNode(value)].getInterval();
|
|
256
|
+
if (idxItv.isBottom())
|
|
257
|
+
idxLb = idxUb = 0;
|
|
258
|
+
else
|
|
259
|
+
{
|
|
260
|
+
idxLb = idxItv.lb().getIntNumeral();
|
|
261
|
+
idxUb = idxItv.ub().getIntNumeral();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Adjust the bounds if the type is a pointer
|
|
266
|
+
if (SVFUtil::isa<SVFPointerType>(type))
|
|
267
|
+
{
|
|
268
|
+
u32_t elemNum = gep->getAccessPath().getElementNum(gep->getAccessPath().gepSrcPointeeType());
|
|
269
|
+
idxLb = (double)Options::MaxFieldLimit() / elemNum < idxLb ? Options::MaxFieldLimit() : idxLb * elemNum;
|
|
270
|
+
idxUb = (double)Options::MaxFieldLimit() / elemNum < idxUb ? Options::MaxFieldLimit() : idxUb * elemNum;
|
|
271
|
+
}
|
|
272
|
+
// Adjust the bounds for array or struct types using the symbol table info
|
|
273
|
+
else
|
|
274
|
+
{
|
|
275
|
+
if (Options::ModelArrays())
|
|
276
|
+
{
|
|
277
|
+
const std::vector<u32_t>& so = SymbolTableInfo::SymbolInfo()->getTypeInfo(type)->getFlattenedElemIdxVec();
|
|
278
|
+
if (so.empty() || idxUb >= (APOffset)so.size() || idxLb < 0)
|
|
279
|
+
{
|
|
280
|
+
idxLb = idxUb = 0;
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
{
|
|
284
|
+
idxLb = SymbolTableInfo::SymbolInfo()->getFlattenedElemIdx(type, idxLb);
|
|
285
|
+
idxUb = SymbolTableInfo::SymbolInfo()->getFlattenedElemIdx(type, idxUb);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else
|
|
289
|
+
idxLb = idxUb = 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Add the calculated interval to the result
|
|
293
|
+
res = res + IntervalValue(idxLb, idxUb);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Ensure the result is within the bounds of [0, MaxFieldLimit]
|
|
297
|
+
res.meet_with(IntervalValue((s64_t)0, (s64_t)Options::MaxFieldLimit()));
|
|
298
|
+
if (res.isBottom())
|
|
299
|
+
{
|
|
300
|
+
res = IntervalValue(0);
|
|
301
|
+
}
|
|
302
|
+
return res;
|
|
303
|
+
}
|
|
304
|
+
// getByteOffset
|
|
305
|
+
IntervalValue AbstractState::getByteOffset(const GepStmt* gep)
|
|
196
306
|
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
307
|
+
// If the GEP statement has a constant byte offset, return it directly as the interval value
|
|
308
|
+
if (gep->isConstantOffset())
|
|
309
|
+
return IntervalValue((s64_t)gep->accumulateConstantByteOffset());
|
|
310
|
+
|
|
311
|
+
IntervalValue res(0); // Initialize the result interval 'res' to 0.
|
|
312
|
+
|
|
313
|
+
// Loop through the offsetVarAndGepTypePairVec in reverse order.
|
|
314
|
+
for (int i = gep->getOffsetVarAndGepTypePairVec().size() - 1; i >= 0; i--)
|
|
315
|
+
{
|
|
316
|
+
const SVFVar* idxOperandVar = gep->getOffsetVarAndGepTypePairVec()[i].first;
|
|
317
|
+
const SVFType* idxOperandType = gep->getOffsetVarAndGepTypePairVec()[i].second;
|
|
318
|
+
|
|
319
|
+
// Calculate the byte offset for array or pointer types
|
|
320
|
+
if (SVFUtil::isa<SVFArrayType>(idxOperandType) || SVFUtil::isa<SVFPointerType>(idxOperandType))
|
|
321
|
+
{
|
|
322
|
+
u32_t elemByteSize = 1;
|
|
323
|
+
if (const SVFArrayType* arrOperandType = SVFUtil::dyn_cast<SVFArrayType>(idxOperandType))
|
|
324
|
+
elemByteSize = arrOperandType->getTypeOfElement()->getByteSize();
|
|
325
|
+
else if (SVFUtil::isa<SVFPointerType>(idxOperandType))
|
|
326
|
+
elemByteSize = gep->getAccessPath().gepSrcPointeeType()->getByteSize();
|
|
327
|
+
else
|
|
328
|
+
assert(false && "idxOperandType must be ArrType or PtrType");
|
|
329
|
+
|
|
330
|
+
if (const SVFConstantInt* op = SVFUtil::dyn_cast<SVFConstantInt>(idxOperandVar->getValue()))
|
|
331
|
+
{
|
|
332
|
+
// Calculate the lower bound (lb) of the interval value
|
|
333
|
+
s64_t lb = (double)Options::MaxFieldLimit() / elemByteSize >= op->getSExtValue()
|
|
334
|
+
? op->getSExtValue() * elemByteSize
|
|
335
|
+
: Options::MaxFieldLimit();
|
|
336
|
+
res = res + IntervalValue(lb, lb);
|
|
337
|
+
}
|
|
338
|
+
else
|
|
339
|
+
{
|
|
340
|
+
u32_t idx = PAG::getPAG()->getValueNode(idxOperandVar->getValue());
|
|
341
|
+
IntervalValue idxVal = (*this)[idx].getInterval();
|
|
342
|
+
|
|
343
|
+
if (idxVal.isBottom())
|
|
344
|
+
res = res + IntervalValue(0, 0);
|
|
345
|
+
else
|
|
346
|
+
{
|
|
347
|
+
// Ensure the bounds are non-negative and within the field limit
|
|
348
|
+
s64_t ub = (idxVal.ub().getIntNumeral() < 0) ? 0
|
|
349
|
+
: (double)Options::MaxFieldLimit() / elemByteSize >= idxVal.ub().getIntNumeral()
|
|
350
|
+
? elemByteSize * idxVal.ub().getIntNumeral()
|
|
351
|
+
: Options::MaxFieldLimit();
|
|
352
|
+
s64_t lb = (idxVal.lb().getIntNumeral() < 0) ? 0
|
|
353
|
+
: (double)Options::MaxFieldLimit() / elemByteSize >= idxVal.lb().getIntNumeral()
|
|
354
|
+
? elemByteSize * idxVal.lb().getIntNumeral()
|
|
355
|
+
: Options::MaxFieldLimit();
|
|
356
|
+
res = res + IntervalValue(lb, ub);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Process struct subtypes by calculating the byte offset from the beginning to the field of the struct
|
|
361
|
+
else if (const SVFStructType* structOperandType = SVFUtil::dyn_cast<SVFStructType>(idxOperandType))
|
|
362
|
+
{
|
|
363
|
+
res = res + IntervalValue(gep->getAccessPath().getStructFieldOffset(idxOperandVar, structOperandType));
|
|
364
|
+
}
|
|
365
|
+
else
|
|
366
|
+
{
|
|
367
|
+
assert(false && "gep type pair only support arr/ptr/struct");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return res; // Return the resulting byte offset as an IntervalValue.
|
|
201
371
|
}
|
|
202
372
|
|
|
203
|
-
|
|
373
|
+
AbstractValue AbstractState::loadValue(NodeID varId)
|
|
204
374
|
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
for (const auto &item: table)
|
|
375
|
+
AbstractValue res;
|
|
376
|
+
for (auto addr : (*this)[varId].getAddrs())
|
|
208
377
|
{
|
|
209
|
-
|
|
378
|
+
res.join_with(load(addr)); // q = *p
|
|
210
379
|
}
|
|
211
|
-
|
|
380
|
+
return res;
|
|
381
|
+
}
|
|
382
|
+
// storeValue
|
|
383
|
+
void AbstractState::storeValue(NodeID varId, AbstractValue val)
|
|
384
|
+
{
|
|
385
|
+
for (auto addr : (*this)[varId].getAddrs())
|
|
212
386
|
{
|
|
213
|
-
|
|
214
|
-
oss << "\t Value: " << table.at(item).toString() << "\n";
|
|
387
|
+
store(addr, val); // *p = q
|
|
215
388
|
}
|
|
216
389
|
}
|
|
390
|
+
|
|
391
|
+
void AbstractState::printAbstractState() const
|
|
392
|
+
{
|
|
393
|
+
SVFUtil::outs() << "-----------Var and Value-----------\n";
|
|
394
|
+
u32_t fieldWidth = 20;
|
|
395
|
+
SVFUtil::outs().flags(std::ios::left);
|
|
396
|
+
std::vector<std::pair<u32_t, AbstractValue>> varToAbsValVec(_varToAbsVal.begin(), _varToAbsVal.end());
|
|
397
|
+
std::sort(varToAbsValVec.begin(), varToAbsValVec.end(), [](const auto &a, const auto &b)
|
|
398
|
+
{
|
|
399
|
+
return a.first < b.first;
|
|
400
|
+
});
|
|
401
|
+
for (const auto &item: varToAbsValVec)
|
|
402
|
+
{
|
|
403
|
+
SVFUtil::outs() << std::left << std::setw(fieldWidth) << ("Var" + std::to_string(item.first));
|
|
404
|
+
if (item.second.isInterval())
|
|
405
|
+
{
|
|
406
|
+
SVFUtil::outs() << " Value: " << item.second.getInterval().toString() << "\n";
|
|
407
|
+
}
|
|
408
|
+
else if (item.second.isAddr())
|
|
409
|
+
{
|
|
410
|
+
SVFUtil::outs() << " Value: {";
|
|
411
|
+
u32_t i = 0;
|
|
412
|
+
for (const auto& addr: item.second.getAddrs())
|
|
413
|
+
{
|
|
414
|
+
++i;
|
|
415
|
+
if (i < item.second.getAddrs().size())
|
|
416
|
+
{
|
|
417
|
+
SVFUtil::outs() << "0x" << std::hex << addr << ", ";
|
|
418
|
+
}
|
|
419
|
+
else
|
|
420
|
+
{
|
|
421
|
+
SVFUtil::outs() << "0x" << std::hex << addr;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
SVFUtil::outs() << "}\n";
|
|
425
|
+
}
|
|
426
|
+
else
|
|
427
|
+
{
|
|
428
|
+
SVFUtil::outs() << " Value: ⊥\n";
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
std::vector<std::pair<u32_t, AbstractValue>> addrToAbsValVec(_addrToAbsVal.begin(), _addrToAbsVal.end());
|
|
433
|
+
std::sort(addrToAbsValVec.begin(), addrToAbsValVec.end(), [](const auto &a, const auto &b)
|
|
434
|
+
{
|
|
435
|
+
return a.first < b.first;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
for (const auto& item: addrToAbsValVec)
|
|
439
|
+
{
|
|
440
|
+
std::ostringstream oss;
|
|
441
|
+
oss << "0x" << std::hex << AbstractState::getVirtualMemAddress(item.first);
|
|
442
|
+
SVFUtil::outs() << std::left << std::setw(fieldWidth) << oss.str();
|
|
443
|
+
if (item.second.isInterval())
|
|
444
|
+
{
|
|
445
|
+
SVFUtil::outs() << " Value: " << item.second.getInterval().toString() << "\n";
|
|
446
|
+
}
|
|
447
|
+
else if (item.second.isAddr())
|
|
448
|
+
{
|
|
449
|
+
SVFUtil::outs() << " Value: {";
|
|
450
|
+
u32_t i = 0;
|
|
451
|
+
for (const auto& addr: item.second.getAddrs())
|
|
452
|
+
{
|
|
453
|
+
++i;
|
|
454
|
+
if (i < item.second.getAddrs().size())
|
|
455
|
+
{
|
|
456
|
+
SVFUtil::outs() << "0x" << std::hex << addr << ", ";
|
|
457
|
+
}
|
|
458
|
+
else
|
|
459
|
+
{
|
|
460
|
+
SVFUtil::outs() << "0x" << std::hex << addr;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
SVFUtil::outs() << "}\n";
|
|
464
|
+
}
|
|
465
|
+
else
|
|
466
|
+
{
|
|
467
|
+
SVFUtil::outs() << " Value: ⊥\n";
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
SVFUtil::outs() << "-----------------------------------------\n";
|
|
471
|
+
}
|