|
ds@2392
|
1 |
// ----------------------------------------------------------------------------- |
|
ds@2392
|
2 |
// - IDA Pro Script - |
|
ds@2392
|
3 |
// Name: gcc_fpic.idc |
|
ds@2392
|
4 |
// By: Damaged Soul |
|
ds@2392
|
5 |
// Desc: Add references for strings, variables, and other data that seem mangled |
|
ds@2392
|
6 |
// due to GCC's -fPIC option and the .got section of an x86 ELF binary. |
|
ds@2392
|
7 |
// |
|
ds@2392
|
8 |
// Version History |
|
ds@2392
|
9 |
// 1.0 [2007-11-22] |
|
ds@2392
|
10 |
// - Initial Version |
|
ds@2392
|
11 |
// 1.1 [2008-05-02] |
|
ds@2392
|
12 |
// - Now works with GCC 4.x compiled binaries |
|
ds@2392
|
13 |
// 1.2 [2008-11-06] |
|
ds@2392
|
14 |
// - Now works with GCC 4.3 compiled binaries |
|
ds@2392
|
15 |
// - Fixed: Redefining alignment blocks as data caused IDA to pop up |
|
ds@2392
|
16 |
// an annoying warning |
|
ds@2392
|
17 |
// ----------------------------------------------------------------------------- |
|
ds@2392
|
18 |
|
|
ds@2392
|
19 |
#include <idc.idc> |
|
ds@2392
|
20 |
|
|
ds@2392
|
21 |
#define REG_NONE 0 |
|
ds@2392
|
22 |
#define REG_EAX 1 |
|
ds@2392
|
23 |
#define REG_EBX 2 |
|
ds@2392
|
24 |
#define REG_ECX 3 |
|
ds@2392
|
25 |
#define REG_EDX 4 |
|
ds@2392
|
26 |
|
|
ds@2392
|
27 |
#define OP_ADD 1 |
|
ds@2392
|
28 |
#define OP_SUB 2 |
|
ds@2392
|
29 |
|
|
ds@2392
|
30 |
#define OPFORMAT_STRING 1 |
|
ds@2392
|
31 |
#define OPFORMAT_DEREF 2 |
|
ds@2392
|
32 |
#define OPFORMAT_NORMAL 3 |
|
ds@2392
|
33 |
|
|
ds@2392
|
34 |
static main() |
|
ds@2392
|
35 |
{ |
|
ds@2392
|
36 |
auto filetype, compiler, demang, strPrefix; |
|
ds@2392
|
37 |
auto seg, codeStart, codeEnd, roStart, roEnd, gotStart, gotEnd, opformat; |
|
ds@2392
|
38 |
auto addr, funcend, whichop, reg, tempstr; |
|
ds@2392
|
39 |
auto operand1, operand2, opval, opstr, dataAddr, flags, count; |
|
ds@2392
|
40 |
|
|
ds@2392
|
41 |
SetStatus(IDA_STATUS_WORK); |
|
ds@2392
|
42 |
Message("Starting scan for -fPIC code...\n"); |
|
ds@2392
|
43 |
|
|
ds@2392
|
44 |
/* Check file type and compiler */ |
|
ds@2392
|
45 |
filetype = GetShortPrm(INF_FILETYPE); |
|
ds@2392
|
46 |
if (filetype != FT_ELF) |
|
ds@2392
|
47 |
{ |
|
ds@2392
|
48 |
Message("Scan aborted. Input file must be using ELF binary format!\n"); |
|
ds@2392
|
49 |
SetStatus(IDA_STATUS_READY); |
|
ds@2392
|
50 |
return; |
|
ds@2392
|
51 |
} |
|
ds@2392
|
52 |
compiler = GetCharPrm(INF_COMPILER); |
|
ds@2392
|
53 |
if (compiler != COMP_GNU) |
|
ds@2392
|
54 |
{ |
|
ds@2392
|
55 |
Message("Scan aborted. Input file must have been compiled with GNU GCC/G++!\n"); |
|
ds@2392
|
56 |
SetStatus(IDA_STATUS_READY); |
|
ds@2392
|
57 |
return; |
|
ds@2392
|
58 |
} |
|
ds@2392
|
59 |
|
|
ds@2392
|
60 |
/* |
|
ds@2392
|
61 |
* If the GCC v3.x names option is not set, then set it first. |
|
ds@2392
|
62 |
* |
|
ds@2392
|
63 |
* :TODO: Need to change this if GCC 2.95 support is to be added. |
|
ds@2392
|
64 |
*/ |
|
ds@2392
|
65 |
demang = GetCharPrm(INF_DEMNAMES); |
|
ds@2392
|
66 |
if ((demang & DEMNAM_GCC3) == 0) |
|
ds@2392
|
67 |
{ |
|
ds@2392
|
68 |
SetCharPrm(INF_DEMNAMES, demang | DEMNAM_GCC3); |
|
ds@2392
|
69 |
} |
|
ds@2392
|
70 |
|
|
ds@2392
|
71 |
/* Get string prefix */ |
|
ds@2392
|
72 |
strPrefix = GetCharPrm(INF_ASCIIPREF); |
|
ds@2392
|
73 |
|
|
ds@2392
|
74 |
/* Get address of first section in binary */ |
|
ds@2392
|
75 |
seg = FirstSeg(); |
|
ds@2392
|
76 |
|
|
ds@2392
|
77 |
/* Iterate through all sections and get address of .text, .rodata, and .got */ |
|
ds@2392
|
78 |
while (seg != BADADDR) |
|
ds@2392
|
79 |
{ |
|
ds@2392
|
80 |
if (SegName(seg) == ".text") |
|
ds@2392
|
81 |
{ |
|
ds@2392
|
82 |
codeStart = seg; |
|
ds@2392
|
83 |
codeEnd = NextSeg(seg); |
|
ds@2392
|
84 |
Message("%08.8Xh - %08.8Xh: .text\n", codeStart, codeEnd); |
|
ds@2392
|
85 |
} |
|
ds@2392
|
86 |
else if (SegName(seg) == ".rodata") |
|
ds@2392
|
87 |
{ |
|
ds@2392
|
88 |
roStart = seg; |
|
ds@2392
|
89 |
roEnd = NextSeg(seg); |
|
ds@2392
|
90 |
Message("%08.8Xh - %08.8Xh: .rodata\n", roStart, roEnd); |
|
ds@2392
|
91 |
} |
|
ds@2392
|
92 |
else if (SegName(seg) == "abs") |
|
ds@2392
|
93 |
{ |
|
ds@2392
|
94 |
addr = FindText(seg, SEARCH_DOWN|SEARCH_CASE|SEARCH_NOSHOW, 0, 0, "_GLOBAL_OFFSET_TABLE_"); |
|
ds@2392
|
95 |
gotStart = Dword(addr); |
|
ds@2392
|
96 |
gotEnd = NextSeg(gotStart); |
|
ds@2392
|
97 |
Message("%08.8Xh - %08.8Xh: .got\n", gotStart, gotEnd); |
|
ds@2392
|
98 |
} |
|
ds@2392
|
99 |
|
|
ds@2392
|
100 |
seg = NextSeg(seg); |
|
ds@2392
|
101 |
} |
|
ds@2392
|
102 |
|
|
ds@2392
|
103 |
addr = codeStart; |
|
ds@2392
|
104 |
funcend = -1; |
|
ds@2392
|
105 |
count = 0; |
|
ds@2392
|
106 |
|
|
ds@2392
|
107 |
/** |
|
ds@2392
|
108 |
* Go through .text section while looking for anything like [e?x+blah] or [e?x-blah]. |
|
ds@2392
|
109 |
* |
|
ds@2392
|
110 |
* The eax, ebx, ecx, or edx registers are used for storing the address of the .got |
|
ds@2392
|
111 |
* section with -fPIC code. An offset, either negative or positive is added to e?x. |
|
ds@2392
|
112 |
* This results in the address of a string, variable, or other type of data. |
|
ds@2392
|
113 |
* |
|
ds@2392
|
114 |
* In order to determine which register .got will be stored in, one can look at which |
|
ds@2392
|
115 |
* __i686.get_pc_thunk.? function is called near the beginning of each function. The |
|
ds@2392
|
116 |
* suffix on this function is either ax, bx, cx, or dx and corresponds to eax, ebx, |
|
ds@2392
|
117 |
* ecx, or edx. |
|
ds@2392
|
118 |
*/ |
|
ds@2392
|
119 |
while (addr <= codeEnd) |
|
ds@2392
|
120 |
{ |
|
ds@2392
|
121 |
operand1 = GetOpnd(addr, 0); |
|
ds@2392
|
122 |
operand2 = GetOpnd(addr, 1); |
|
ds@2392
|
123 |
whichop = -1; |
|
ds@2392
|
124 |
|
|
ds@2392
|
125 |
/* Get function end */ |
|
ds@2392
|
126 |
if (FindFuncEnd(addr) != funcend) |
|
ds@2392
|
127 |
{ |
|
ds@2392
|
128 |
reg = REG_NONE; |
|
ds@2392
|
129 |
funcend = FindFuncEnd(addr); |
|
ds@2392
|
130 |
} |
|
ds@2392
|
131 |
|
|
ds@2392
|
132 |
/* Get current PIC register */ |
|
ds@2392
|
133 |
reg = GetPICRegister(addr, reg, funcend); |
|
ds@2392
|
134 |
|
|
ds@2392
|
135 |
if (reg != REG_NONE) |
|
ds@2392
|
136 |
{ |
|
ds@2392
|
137 |
/* Search first operand for substring containing PIC register and either a plus or minus sign */ |
|
ds@2392
|
138 |
if (strstr(operand1, GetPICSearchString(reg, OP_ADD)) != -1 || strstr(operand1, GetPICSearchString(reg, OP_SUB)) != -1) |
|
ds@2392
|
139 |
{ |
|
ds@2392
|
140 |
whichop = 0; |
|
ds@2392
|
141 |
} |
|
ds@2392
|
142 |
|
|
ds@2392
|
143 |
/* Search second operand for substring containing PIC register and either a plus or minus sign */ |
|
ds@2392
|
144 |
if (strstr(operand2, GetPICSearchString(reg, OP_ADD)) != -1 || strstr(operand2, GetPICSearchString(reg, OP_SUB)) != -1) |
|
ds@2392
|
145 |
{ |
|
ds@2392
|
146 |
whichop = 1; |
|
ds@2392
|
147 |
} |
|
ds@2392
|
148 |
} |
|
ds@2392
|
149 |
|
|
ds@2392
|
150 |
if (whichop != -1) |
|
ds@2392
|
151 |
{ |
|
ds@2392
|
152 |
/* Get .got offset */ |
|
ds@2392
|
153 |
opval = GetOperandValue(addr, whichop); |
|
ds@2392
|
154 |
|
|
ds@2392
|
155 |
/* Get address inside .got */ |
|
ds@2392
|
156 |
dataAddr = gotStart + opval; |
|
ds@2392
|
157 |
|
|
ds@2392
|
158 |
/* Get name at address if it exists */ |
|
ds@2392
|
159 |
opstr = Name(dataAddr); |
|
ds@2392
|
160 |
|
|
ds@2392
|
161 |
/* If name doesn't exist then... */ |
|
ds@2392
|
162 |
if (opstr == "") |
|
ds@2392
|
163 |
{ |
|
ds@2392
|
164 |
/* |
|
ds@2392
|
165 |
* Check address to see if it falls in .rodata section. |
|
ds@2392
|
166 |
* If it does, then try to make it a string which will automatically give it a name. |
|
ds@2392
|
167 |
*/ |
|
ds@2392
|
168 |
if (dataAddr >= roStart && dataAddr <= roEnd && MakeStr(dataAddr, BADADDR)) |
|
ds@2392
|
169 |
{ |
|
ds@2392
|
170 |
/* Get automatically created name */ |
|
ds@2392
|
171 |
opstr = Name(dataAddr); |
|
ds@2392
|
172 |
opformat = OPFORMAT_STRING; |
|
ds@2392
|
173 |
|
|
ds@2392
|
174 |
/* |
|
ds@2392
|
175 |
* Sometimes IDA creates a string successfully but not exactly in the right place. |
|
ds@2392
|
176 |
* Uncertain as to why this is (perhaps an IDA bug?), but usually the string in |
|
ds@2392
|
177 |
* question is a bunch of garbage. |
|
ds@2392
|
178 |
*/ |
|
ds@2392
|
179 |
if (opstr == "") |
|
ds@2392
|
180 |
{ |
|
ds@2392
|
181 |
/* Create a name based on the address */ |
|
ds@2392
|
182 |
opstr = form("unk_%X", dataAddr); |
|
ds@2392
|
183 |
if (strstr(GetDisasm(dataAddr), "align") != -1) |
|
ds@2392
|
184 |
{ |
|
ds@2392
|
185 |
MakeUnkn(dataAddr, DOUNK_SIMPLE); |
|
ds@2392
|
186 |
} |
|
ds@2392
|
187 |
MakeNameEx(dataAddr, opstr, SN_NOCHECK|SN_NOLIST|SN_NOWARN); |
|
ds@2392
|
188 |
opformat = OPFORMAT_DEREF; |
|
ds@2392
|
189 |
} |
|
ds@2392
|
190 |
} |
|
ds@2392
|
191 |
else |
|
ds@2392
|
192 |
{ |
|
ds@2392
|
193 |
/* |
|
ds@2392
|
194 |
* If address didn't fall into .rodata and the string creation was unsuccessful, |
|
ds@2392
|
195 |
* then try to read the address at 'addr' and get the name of that. |
|
ds@2392
|
196 |
*/ |
|
ds@2392
|
197 |
opstr = Name(Dword(dataAddr)); |
|
ds@2392
|
198 |
if (opstr == "") |
|
ds@2392
|
199 |
{ |
|
ds@2392
|
200 |
/* If name doesn't exist for that, then create name based on address */ |
|
ds@2392
|
201 |
opstr = form("unk_%X", dataAddr); |
|
ds@2392
|
202 |
if (strstr(GetDisasm(dataAddr), "align") != -1) |
|
ds@2392
|
203 |
{ |
|
ds@2392
|
204 |
MakeUnkn(dataAddr, DOUNK_SIMPLE); |
|
ds@2392
|
205 |
} |
|
ds@2392
|
206 |
MakeNameEx(dataAddr, opstr, SN_NOCHECK|SN_NOLIST|SN_NOWARN); |
|
ds@2392
|
207 |
opformat = OPFORMAT_DEREF; |
|
ds@2392
|
208 |
} |
|
ds@2392
|
209 |
else |
|
ds@2392
|
210 |
{ |
|
ds@2392
|
211 |
/* If the name did exist at this point, then use it */ |
|
ds@2392
|
212 |
opformat = OPFORMAT_NORMAL; |
|
ds@2392
|
213 |
} |
|
ds@2392
|
214 |
} |
|
ds@2392
|
215 |
} |
|
ds@2392
|
216 |
else |
|
ds@2392
|
217 |
{ |
|
ds@2392
|
218 |
/* If the name at the original address does exist then ... */ |
|
ds@2392
|
219 |
|
|
ds@2392
|
220 |
flags = GetFlags(dataAddr); |
|
ds@2392
|
221 |
|
|
ds@2392
|
222 |
/* |
|
ds@2392
|
223 |
* If this address falls into .rodata section and is considered an existing string |
|
ds@2392
|
224 |
* then the replacement operand needs to shown as a string. |
|
ds@2392
|
225 |
*/ |
|
ds@2392
|
226 |
if (dataAddr >= roStart && dataAddr <= roEnd && strPrefix != "" && strstr(opstr, strPrefix) == 0) |
|
ds@2392
|
227 |
{ |
|
ds@2392
|
228 |
opformat = OPFORMAT_STRING; |
|
ds@2392
|
229 |
} |
|
ds@2392
|
230 |
else |
|
ds@2392
|
231 |
{ |
|
ds@2392
|
232 |
opformat = OPFORMAT_DEREF; |
|
ds@2392
|
233 |
} |
|
ds@2392
|
234 |
} |
|
ds@2392
|
235 |
|
|
ds@2392
|
236 |
/* |
|
ds@2392
|
237 |
* Try to demangle the name that was found or created above and print it as sort of |
|
ds@2392
|
238 |
* a status message to show the the script is still doing work as this usually |
|
ds@2392
|
239 |
* can take awhile. |
|
ds@2392
|
240 |
*/ |
|
ds@2392
|
241 |
tempstr = Demangle(opstr, INF_LONG_DN); |
|
ds@2392
|
242 |
if (tempstr != "") |
|
ds@2392
|
243 |
{ |
|
ds@2392
|
244 |
Message("%8.8Xh: %s\n", addr, tempstr); |
|
ds@2392
|
245 |
} |
|
ds@2392
|
246 |
else |
|
ds@2392
|
247 |
{ |
|
ds@2392
|
248 |
Message("%8.8Xh: %s\n", addr, opstr); |
|
ds@2392
|
249 |
} |
|
ds@2392
|
250 |
|
|
ds@2392
|
251 |
/* |
|
ds@2392
|
252 |
* The operand that was found to have the PIC register will now be replaced |
|
ds@2392
|
253 |
* with more descriptive text. The format of this text depends upon the value |
|
ds@2392
|
254 |
* of opformat. |
|
ds@2392
|
255 |
*/ |
|
ds@2392
|
256 |
OpAlt(addr, whichop, DoOperandFormat(opformat, opstr)); |
|
ds@2392
|
257 |
|
|
ds@2392
|
258 |
count++; |
|
ds@2392
|
259 |
} |
|
ds@2392
|
260 |
|
|
ds@2392
|
261 |
addr++; |
|
ds@2392
|
262 |
} |
|
ds@2392
|
263 |
|
|
ds@2392
|
264 |
Message("Scan for PIC code is complete! Found %d data items referenced via PIC register.\n", count); |
|
ds@2392
|
265 |
Message("Please re-open the database so that newly found strings will appear in the Strings window\n"); |
|
ds@2392
|
266 |
SetStatus(IDA_STATUS_READY); |
|
ds@2392
|
267 |
} |
|
ds@2392
|
268 |
|
|
ds@2392
|
269 |
/* |
|
ds@2392
|
270 |
* Tries to determine the current PIC register given the current address being processed |
|
ds@2392
|
271 |
* and the previous PIC register. |
|
ds@2392
|
272 |
*/ |
|
ds@2392
|
273 |
static GetPICRegister(addr, previous, funcend) |
|
ds@2392
|
274 |
{ |
|
ds@2392
|
275 |
auto assemblyStr, idx, reg, ab; |
|
ds@2392
|
276 |
assemblyStr = GetDisasm(addr); |
|
ds@2392
|
277 |
|
|
ds@2392
|
278 |
if ((idx = strstr(assemblyStr, "call __i686_get_pc_thunk_")) != -1) |
|
ds@2392
|
279 |
{ |
|
ds@2392
|
280 |
/* 28 is the length of the above string */ |
|
ds@2392
|
281 |
reg = substr(assemblyStr, idx + 28, 30); |
|
ds@2392
|
282 |
} |
|
ds@2392
|
283 |
else if (strstr(assemblyStr, "call $+5") != -1) |
|
ds@2392
|
284 |
{ |
|
ds@2392
|
285 |
assemblyStr = GetDisasm(NextHead(addr, funcend)); |
|
ds@2392
|
286 |
reg = substr(assemblyStr, 9, 11); |
|
ds@2392
|
287 |
} |
|
ds@2392
|
288 |
|
|
ds@2392
|
289 |
if (reg == "ax") |
|
ds@2392
|
290 |
{ |
|
ds@2392
|
291 |
return REG_EAX; |
|
ds@2392
|
292 |
} |
|
ds@2392
|
293 |
else if (reg == "bx") |
|
ds@2392
|
294 |
{ |
|
ds@2392
|
295 |
return REG_EBX; |
|
ds@2392
|
296 |
} |
|
ds@2392
|
297 |
else if (reg == "cx") |
|
ds@2392
|
298 |
{ |
|
ds@2392
|
299 |
return REG_ECX; |
|
ds@2392
|
300 |
} |
|
ds@2392
|
301 |
else if (reg == "dx") |
|
ds@2392
|
302 |
{ |
|
ds@2392
|
303 |
return REG_EDX; |
|
ds@2392
|
304 |
} |
|
ds@2392
|
305 |
|
|
ds@2392
|
306 |
return previous; |
|
ds@2392
|
307 |
} |
|
ds@2392
|
308 |
|
|
ds@2392
|
309 |
/* |
|
ds@2392
|
310 |
* Returns a string that is used as a substring search containing the specified |
|
ds@2392
|
311 |
* PIC register and operator (+ or -). |
|
ds@2392
|
312 |
*/ |
|
ds@2392
|
313 |
static GetPICSearchString(reg, operator) |
|
ds@2392
|
314 |
{ |
|
ds@2392
|
315 |
if (reg == REG_EAX) |
|
ds@2392
|
316 |
{ |
|
ds@2392
|
317 |
if (operator == OP_ADD) |
|
ds@2392
|
318 |
{ |
|
ds@2392
|
319 |
return "[eax+"; |
|
ds@2392
|
320 |
} |
|
ds@2392
|
321 |
else if (operator == OP_SUB) |
|
ds@2392
|
322 |
{ |
|
ds@2392
|
323 |
return "[eax-"; |
|
ds@2392
|
324 |
} |
|
ds@2392
|
325 |
} |
|
ds@2392
|
326 |
else if (reg == REG_EBX) |
|
ds@2392
|
327 |
{ |
|
ds@2392
|
328 |
if (operator == OP_ADD) |
|
ds@2392
|
329 |
{ |
|
ds@2392
|
330 |
return "[ebx+"; |
|
ds@2392
|
331 |
} |
|
ds@2392
|
332 |
else if (operator == OP_SUB) |
|
ds@2392
|
333 |
{ |
|
ds@2392
|
334 |
return "[ebx-"; |
|
ds@2392
|
335 |
} |
|
ds@2392
|
336 |
} |
|
ds@2392
|
337 |
else if (reg == REG_ECX) |
|
ds@2392
|
338 |
{ |
|
ds@2392
|
339 |
if (operator == OP_ADD) |
|
ds@2392
|
340 |
{ |
|
ds@2392
|
341 |
return "[ecx+"; |
|
ds@2392
|
342 |
} |
|
ds@2392
|
343 |
else if (operator == OP_SUB) |
|
ds@2392
|
344 |
{ |
|
ds@2392
|
345 |
return "[ecx-"; |
|
ds@2392
|
346 |
} |
|
ds@2392
|
347 |
} |
|
ds@2392
|
348 |
else if (reg == REG_EDX) |
|
ds@2392
|
349 |
{ |
|
ds@2392
|
350 |
if (operator == OP_ADD) |
|
ds@2392
|
351 |
{ |
|
ds@2392
|
352 |
return "[edx+"; |
|
ds@2392
|
353 |
} |
|
ds@2392
|
354 |
else if (operator == OP_SUB) |
|
ds@2392
|
355 |
{ |
|
ds@2392
|
356 |
return "[edx-"; |
|
ds@2392
|
357 |
} |
|
ds@2392
|
358 |
} |
|
ds@2392
|
359 |
} |
|
ds@2392
|
360 |
|
|
ds@2392
|
361 |
/* |
|
ds@2392
|
362 |
* Returns a formatted string depending upon the value of the format param. |
|
ds@2392
|
363 |
* This will be the replacement for the operand containing the PIC register. |
|
ds@2392
|
364 |
* |
|
ds@2392
|
365 |
* OPFORMAT_STRING: The referenced data address is a string in the .rodata section. |
|
ds@2392
|
366 |
* OPFORMAT_DEREF: The referenced data address has a name. The PIC register operand is |
|
ds@2392
|
367 |
* deferenced so the dereference brackets are shown in the returned string. |
|
ds@2392
|
368 |
* OPFORMAT_NORMAL: The referenced data address does not have a name. But upon reading |
|
ds@2392
|
369 |
* the address at the referenced data address, it is discovered that that |
|
ds@2392
|
370 |
* address does have a name. There are no dereference brackets because the |
|
ds@2392
|
371 |
* referenced data address had to be read in order to discover a name. |
|
ds@2392
|
372 |
*/ |
|
ds@2392
|
373 |
static DoOperandFormat(format, str) |
|
ds@2392
|
374 |
{ |
|
ds@2392
|
375 |
if (format == OPFORMAT_STRING) |
|
ds@2392
|
376 |
{ |
|
ds@2392
|
377 |
return form("offset %s", str); |
|
ds@2392
|
378 |
} |
|
ds@2392
|
379 |
else if (format == OPFORMAT_DEREF) |
|
ds@2392
|
380 |
{ |
|
ds@2392
|
381 |
return form("[ds:%s]", str); |
|
ds@2392
|
382 |
} |
|
ds@2392
|
383 |
else if (format == OPFORMAT_NORMAL) |
|
ds@2392
|
384 |
{ |
|
ds@2392
|
385 |
return form("ds:%s", str); |
|
ds@2392
|
386 |
} |
|
ds@2392
|
387 |
else |
|
ds@2392
|
388 |
{ |
|
ds@2392
|
389 |
return str; |
|
ds@2392
|
390 |
} |
|
ds@2392
|
391 |
} |