| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891 |
- /*
- ** Command & Conquer Red Alert(tm)
- ** Copyright 2025 Electronic Arts Inc.
- **
- ** This program is free software: you can redistribute it and/or modify
- ** it under the terms of the GNU General Public License as published by
- ** the Free Software Foundation, either version 3 of the License, or
- ** (at your option) any later version.
- **
- ** This program is distributed in the hope that it will be useful,
- ** but WITHOUT ANY WARRANTY; without even the implied warranty of
- ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ** GNU General Public License for more details.
- **
- ** You should have received a copy of the GNU General Public License
- ** along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
- /***********************************************************************************************
- *** C O N F I D E N T I A L --- W E S T W O O D S T U D I O S ***
- ***********************************************************************************************
- * *
- * Project Name : Library profiler *
- * *
- * File Name : PROFILE.CPP *
- * *
- * Programmer : Steve Tall *
- * *
- * Start Date : 11/17/95 *
- * *
- * Last Update : November 20th 1995 [ST] *
- * *
- *---------------------------------------------------------------------------------------------*
- * Overview: *
- * Uses a map file to match addresses of functions in the sample file with their names *
- * *
- * *
- *---------------------------------------------------------------------------------------------*
- * *
- * Functions: *
- * Start_Profiler -- initialises the profiler data and starts gathering data *
- * Stop_Profiler -- stops the timer and writes the profile data to disk *
- * *
- * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
- #include <string.h>
- #include <stdio.h>
- #include <sys\types.h>
- #include <sys\stat.h>
- #include <fcntl.h>
- #include <malloc.h>
- #include <io.h>
- #include <conio.h>
- #define bool int
- #define true 1
- #define false 0
- #define NAME_TABLE_SIZE 1000000 //Storage space for function names
- #define SAMPLE_START 1 //Offset (in dwords) of sample data in sample file
- /*
- ** Function prototypes
- */
- void Print_My_Name(void);
- void Print_Usage(void);
- int Load_File(char *file_name , unsigned *file_ptr , unsigned mode);
- bool Extract_Function_Addresses(void);
- unsigned Get_Hex (char string[] , int length);
- char *Search_For_Char (char character , char buffer_ptr[] , int buffer_length);
- char *Search_For_String (char *string , char *buffer_ptr , int buffer_length);
- void Map_Profiler_Hits (void);
- void Sort_Functions(void);
- void Sort_Functions_Again(void);
- void Output_Profile(void);
- char *SampleFile; //Ptr to sample file name
- char *MapFile; //Ptr to map file name
- unsigned *SampleFileBuffer; //Ptr to buffer that sample file is loaded in to
- char *MapFileBuffer; //Ptr to buffer that map file is loaded in to
- unsigned SampleFileLength; //Length of sample file
- unsigned MapFileLength; //Length of map file
- char FunctionNames[NAME_TABLE_SIZE]; //Buffer to store function names in
- char *FunctionNamePtr=&FunctionNames[0]; //Ptr to end of last function name in buffer
- int TotalFunctions; //Total number of functions extracted from map file
- int SampleRate; //Number of samples/sec that data was collected at
- unsigned EndCodeSegment; //Length of the sampled programs code segments
- /*
- ** Structure for collating function data
- */
- typedef struct tFunction {
- unsigned FunctionAddress; //Address of function relative to start of code seg
- char *FunctionName; //Ptr to name of function in FunctionNames buffer
- int Hits; //Number of times function was 'hit' when sampling
- } Function;
- Function FunctionList[10000]; //max 10,000 functions in map file.
- /***********************************************************************************************
- * main -- program entry point *
- * *
- * *
- * *
- * INPUT: argc , argv *
- * *
- * OUTPUT: 0 *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:21PM ST : Created *
- *=============================================================================================*/
- int main(int argc, char *argv[])
- {
- Print_My_Name(); // print the programs name
- /*
- ** If the arguments dont make sense then print the usage
- */
- if (argc!=3 ||
- !strcmpi(argv[1],"/?") ||
- !strcmpi(argv[1],"/h") ||
- !strcmpi(argv[1],"/help") ||
- !strcmpi(argv[1],"-?") ||
- !strcmpi(argv[1],"-h") ||
- !strcmpi(argv[1],"-help") ||
- !strcmpi(argv[1],"?") ||
- !strcmpi(argv[1],"h") ||
- !strcmpi(argv[1],"help")){
- Print_Usage();
- return(0);
- }
- /*
- ** Get the names of the files to load
- */
- SampleFile=argv[1];
- MapFile=argv[2];
- /*
- ** Load the profile sample file
- */
- SampleFileLength = Load_File (SampleFile , (unsigned*)&SampleFileBuffer , O_BINARY);
- if (!SampleFileLength) return(0);
- /*
- ** The sample rate is the 1st dword in the file
- */
- SampleRate=*SampleFileBuffer;
- /*
- ** Load the .map file
- */
- MapFileLength = Load_File (MapFile , (unsigned*)&MapFileBuffer , O_BINARY);
- if (!MapFileLength){
- free (SampleFileBuffer);
- return(0);
- }
- /*
- ** Get the function names from the map file
- */
- cprintf ("Extracting function data from map file.\n");
- if (!Extract_Function_Addresses()){
- cprintf ("Error parsing .MAP file - aborting\n\n");
- return (0);
- }
- /*
- ** Sort the functions into address order to make it easier to map the functions
- */
- cprintf ("Sorting function list by address");
- Sort_Functions();
- /*
- ** Map the addresses in the sample file to the function addresses
- */
- cprintf ("\nMapping profiler hits to functions");
- Map_Profiler_Hits();
- /*
- ** Sort the functions into order of usage for output
- */
- cprintf ("\nSorting function list by activity");
- Sort_Functions_Again();
- cprintf ("\n\n");
- /*
- ** Print the function usage statistics
- */
- Output_Profile();
- /*
- ** Cleanup and out
- */
- free (SampleFileBuffer);
- free (MapFileBuffer);
- return(0);
- }
- /***********************************************************************************************
- * Print_My_Name -- print the programs name and version *
- * *
- * *
- * INPUT: Nothing *
- * *
- * OUTPUT: Nothing *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:25PM ST : Created *
- *=============================================================================================*/
- void Print_My_Name(void)
- {
- cprintf("Westwood profile data analyzer.\n");
- cprintf("V 1.0 - 11/17/95\n");
- cprintf("Programmer - Steve Tall.\n\n");
- }
- /***********************************************************************************************
- * Print_Usage -- print the instructions *
- * *
- * *
- * INPUT: Nothing *
- * *
- * OUTPUT: Nothing *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:26PM ST : Created *
- *=============================================================================================*/
- void Print_Usage (void)
- {
- cprintf("Usage: PROFILE <sample_file> <map_file)\n\n");
- }
- /***********************************************************************************************
- * File_Error -- display a file error message *
- * *
- * *
- * INPUT: name of file error occurred on *
- * *
- * OUTPUT: Nothing *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:26PM ST : Created *
- *=============================================================================================*/
- void File_Error (char *file_name)
- {
- cprintf ("Error reading file:%s - aborting\n",file_name);
- }
- /***********************************************************************************************
- * Memory_Error -- display an out of memory message *
- * *
- * *
- * *
- * INPUT: Nothing *
- * *
- * OUTPUT: Nothing *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:27PM ST : Created *
- *=============================================================================================*/
- void Memory_Error (void)
- {
- cprintf ("Error - insufficient memory - aborting\n");
- }
- /***********************************************************************************************
- * Load_File -- load an entire file into memory *
- * *
- * *
- * *
- * INPUT: File name *
- * address to load at *
- * read mode (text or binary) *
- * *
- * OUTPUT: number of bytes read *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:27PM ST : Created *
- *=============================================================================================*/
- int Load_File(char *file_name , unsigned *load_addr , unsigned mode)
- {
- int handle;
- unsigned file_length;
- void *buffer;
- handle=open (file_name , O_RDONLY | mode);
- if (handle==-1){
- File_Error(file_name);
- return (false);
- }
- file_length = filelength(handle);
- if (file_length==-1) return (false);
- buffer = malloc (file_length+10);
- if (!buffer){
- Memory_Error();
- return (false);
- }
- if (read (handle , buffer , file_length)!=file_length){
- File_Error(file_name);
- free(buffer);
- return (false);
- }
- close (handle);
- *load_addr = (unsigned)buffer;
- return (file_length);
- }
- /***********************************************************************************************
- * Map_Profiler_Hits -- map function hits from sample file to functions in map file *
- * *
- * *
- * *
- * INPUT: Nothing *
- * *
- * OUTPUT: Nothing *
- * *
- * WARNINGS: Map file functions must be sorted into address order 1st *
- * *
- * HISTORY: *
- * 11/20/95 5:28PM ST : Created *
- *=============================================================================================*/
- void Map_Profiler_Hits (void)
- {
- unsigned *samples=(unsigned*)SampleFileBuffer;
- unsigned function_hit;
- for (int i=SAMPLE_START ; i<SampleFileLength/4 ; i++){
- function_hit=*(samples+i);
- if (1023==(1023 & i)){
- cprintf (".");
- }
- for (int j=TotalFunctions-1 ; j>=0 ; j--){
- if (FunctionList[j].FunctionAddress < function_hit){
- FunctionList[j].Hits++;
- break;
- }
- }
- }
- }
- /***********************************************************************************************
- * Sort_Functions -- hideous bubble sort of functions into address order *
- * *
- * *
- * INPUT: Nothing *
- * *
- * OUTPUT: Nothing *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:29PM ST : Created *
- *=============================================================================================*/
- void Sort_Functions (void)
- {
- Function address_swap;
- if (TotalFunctions>1){
- for (int outer=0 ; outer <TotalFunctions ; outer++){
- address_swap.FunctionAddress=0;
- if (127==(127 & outer)){
- cprintf (".");
- }
- for (int inner=0 ; inner < TotalFunctions-1 ; inner++){
- if (FunctionList[inner].FunctionAddress > FunctionList[inner+1].FunctionAddress ){
- memcpy (&address_swap , &FunctionList[inner] , sizeof(Function));
- memcpy (&FunctionList[inner] , &FunctionList[inner+1] , sizeof(Function));
- memcpy (&FunctionList[inner+1] , &address_swap , sizeof(Function));
- }
- }
- if (!address_swap.FunctionAddress) break;
- }
- }
- }
- /***********************************************************************************************
- * Sort_Functions -- hideous bubble sort of functions into usage order *
- * *
- * *
- * INPUT: Nothing *
- * *
- * OUTPUT: Nothing *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:29PM ST : Created *
- *=============================================================================================*/
- void Sort_Functions_Again (void)
- {
- Function address_swap;
- if (TotalFunctions>1){
- for (int outer=0 ; outer <TotalFunctions ; outer++){
- address_swap.FunctionAddress=0;
- if (127==(127 & outer)){
- cprintf (".");
- }
- for (int inner=0 ; inner < TotalFunctions-1 ; inner++){
- if (FunctionList[inner].Hits < FunctionList[inner+1].Hits ){
- memcpy (&address_swap , &FunctionList[inner] , sizeof(Function));
- memcpy (&FunctionList[inner] , &FunctionList[inner+1] , sizeof(Function));
- memcpy (&FunctionList[inner+1] , &address_swap , sizeof(Function));
- }
- }
- if (!address_swap.FunctionAddress) break;
- }
- }
- }
- /***********************************************************************************************
- * Output_Profile -- output the function data to the screen *
- * *
- * *
- * INPUT: Nothing *
- * *
- * OUTPUT: Nothing *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:31PM ST : Created *
- *=============================================================================================*/
- void Output_Profile(void)
- {
- double period=(((double)SampleFileLength/(double)4) - (double)SAMPLE_START) / (double)SampleRate;
- double percentage;
- printf ( "\n Profile information from %s and %s.\n\n",SampleFile,MapFile);
- printf ( "Samples collected:%d\n" , SampleFileLength/4-SAMPLE_START);
- printf ( "Sample rate :%d samples per second\n",SampleRate);
- printf ( "Sample period :%f seconds\n\n\n" , period);
- printf ( "Hits %% Function\n");
- printf ( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
- for (int i=0 ; i<TotalFunctions ; i++){
- if (FunctionList[i].Hits){
- percentage= ((double)FunctionList[i].Hits*(double)100) / ((double)SampleFileLength/(double)4-(double)SAMPLE_START);
- printf ("%-6d %-3.3f%% %s\n",FunctionList[i].Hits , percentage , FunctionList[i].FunctionName);
- }
- }
- }
- /***********************************************************************************************
- * Extract_Function_Addresses -- gets the addresses of all global functions from a map file *
- * *
- * *
- * INPUT: Nothing *
- * *
- * OUTPUT: Nothing *
- * *
- * WARNINGS: true if successfully extracted *
- * *
- * HISTORY: *
- * 11/20/95 5:31PM ST : Created *
- *=============================================================================================*/
- bool Extract_Function_Addresses(void)
- {
- char *map_ptr;
- char *segment_ptr;
- char *end_str_ptr;
- unsigned chars_left=MapFileLength;
- int function_name_length;
- unsigned end_of_last_code_segment;
- unsigned code_segment_start;
- unsigned code_segment_size;
- char unknown[]={"Windows API or system code."};
- /*
- ** Clear out the list of functions
- */
- memset (&FunctionNames[0] , 0 , NAME_TABLE_SIZE);
- /*
- ** Search for the 'Segments' header in the memory map
- */
- segment_ptr = Search_For_String ("Segments" , MapFileBuffer , chars_left);
- if (!segment_ptr) return (false);
- chars_left = MapFileLength - ( (unsigned)segment_ptr - (unsigned)MapFileBuffer );
- segment_ptr = Search_For_String ("+-----" , segment_ptr , chars_left);
- segment_ptr +=2;
- chars_left = MapFileLength - ( (unsigned)segment_ptr - (unsigned)MapFileBuffer );
- /*
- ** Get the length of the segment section by searching for the start of the next section
- */
- end_str_ptr = Search_For_String ("+-----" , segment_ptr , chars_left);
- if (end_str_ptr){
- chars_left = end_str_ptr - segment_ptr;
- } else {
- return (false);
- }
- EndCodeSegment = 0;
- /*
- ** Find the end of the last code segment
- */
- do {
- /*
- ** Search for a code segment identifier
- */
- chars_left = end_str_ptr - segment_ptr;
- segment_ptr = Search_For_String ("CODE" , segment_ptr , chars_left);
- if (!segment_ptr) break; //No more code segments so break
- /*
- ** Search for the segment address which should always be 0001
- */
- chars_left = end_str_ptr - segment_ptr;
- segment_ptr = Search_For_String ("0001:" , segment_ptr , chars_left);
- if (!segment_ptr) return (false); //Couldnt find the segment address - must be a problem so abort
- /*
- ** Get the start address and length of the segment
- */
- code_segment_start = Get_Hex(segment_ptr+5,8);
- code_segment_size = Get_Hex(segment_ptr+16,8);
- /*
- ** If this segment ends higher in memory than the previous highest then
- ** we have a new last segment
- */
- if (code_segment_start+code_segment_size > EndCodeSegment){
- EndCodeSegment = code_segment_start+code_segment_size;
- }
- chars_left = end_str_ptr - segment_ptr;
- segment_ptr = Search_For_Char ( 13 , segment_ptr , chars_left );
- chars_left = end_str_ptr - segment_ptr;
- } while (chars_left > 0);
- chars_left=MapFileLength;
- /*
- ** Search for the 'Memory Map' segment of the map file
- */
- map_ptr = Search_For_String ("Memory Map" , MapFileBuffer , chars_left);
- if (!map_ptr){
- return (false);
- }
- chars_left = MapFileLength - ( (unsigned)map_ptr - (unsigned)MapFileBuffer );
- /*
- ** Get the length of the memory map segment by searching for the start of the next segment
- */
- end_str_ptr = Search_For_String ("+-----" , map_ptr , chars_left);
- if (end_str_ptr){
- MapFileLength = ((unsigned)MapFileBuffer + MapFileLength) - (unsigned)end_str_ptr;
- }
- chars_left = MapFileLength - ( (unsigned)map_ptr - (unsigned)MapFileBuffer );
- /*
- ** Reset the total number of functions found
- */
- TotalFunctions = 0;
- /*
- **
- ** Find each occurrence of 0001: as all the functions we want are in the 1st segment
- **
- */
- do {
- /*
- ** Find '0001:'
- */
- map_ptr = Search_For_String ("0001:" , map_ptr , chars_left);
- if (!map_ptr){
- break;
- }
- chars_left = MapFileLength - ( (unsigned)map_ptr - (unsigned)MapFileBuffer );
- /*
- ** Skip the '0001:' portion of the address and get the hext offset of the function
- */
- map_ptr+=5;
- FunctionList[TotalFunctions].FunctionAddress=Get_Hex(map_ptr,8);
- /*
- ** Skip to the function name and get its length by searching for the end of the line
- */
- map_ptr+=10;
- chars_left = MapFileLength - ( (unsigned)map_ptr - (unsigned)MapFileBuffer );
- end_str_ptr = Search_For_Char (13 , map_ptr , chars_left);
- if (!end_str_ptr){
- break;
- }
- function_name_length = (unsigned)end_str_ptr - (unsigned)map_ptr;
- /*
- ** Copy the function name into the name list and keep a pointer to it
- */
- memcpy (FunctionNamePtr , map_ptr , function_name_length);
- FunctionList[TotalFunctions].FunctionName = FunctionNamePtr;
- FunctionNamePtr += function_name_length+1; //Leave an extra 0 on the end as a terminator
- FunctionList[TotalFunctions].Hits = 0; //We dont yet know how many times we hit it
- TotalFunctions++;
- } while (1);
- /*
- ** Add in a dummy function at the highest address to represent unknown code hits
- */
- FunctionList[TotalFunctions].FunctionAddress = EndCodeSegment;
- memcpy (FunctionNamePtr , &unknown , sizeof (unknown));
- FunctionList[TotalFunctions].FunctionName = FunctionNamePtr;
- FunctionNamePtr += sizeof (unknown);
- FunctionList[TotalFunctions].Hits = 0;
- TotalFunctions++;
- return (true);
- }
- /***********************************************************************************************
- * Get_Hex -- nasty function to convert an ascii hex number to an unsigned int *
- * I'm sure there must be a lovely 'c' way of doing this but I dont know what it is *
- * *
- * *
- * INPUT: ptr to ascii hex string , number of digits in string *
- * *
- * OUTPUT: value of hex string *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:39PM ST : Created *
- *=============================================================================================*/
- unsigned Get_Hex (char string[] , int length)
- {
- unsigned hex_val=0;
- int multiplier=1;
- char hex_char;
- for (int i=0 ; i<length ; i++){
- hex_char=string[length-1-i];
- switch (hex_char){
- case '0':
- hex_char=0;
- break;
- case '1':
- hex_char=1;
- break;
- case '2':
- hex_char=2;
- break;
- case '3':
- hex_char=3;
- break;
- case '4':
- hex_char=4;
- break;
- case '5':
- hex_char=5;
- break;
- case '6':
- hex_char=6;
- break;
- case '7':
- hex_char=7;
- break;
- case '8':
- hex_char=8;
- break;
- case '9':
- hex_char=9;
- break;
- case 'A':
- hex_char=10;
- break;
- case 'B':
- hex_char=11;
- break;
- case 'C':
- hex_char=12;
- break;
- case 'D':
- hex_char=13;
- break;
- case 'E':
- hex_char=14;
- break;
- case 'F':
- hex_char=15;
- break;
- case 'a':
- hex_char=10;
- break;
- case 'b':
- hex_char=11;
- break;
- case 'c':
- hex_char=12;
- break;
- case 'd':
- hex_char=13;
- break;
- case 'e':
- hex_char=14;
- break;
- case 'f':
- hex_char=15;
- break;
- }
- hex_val += hex_char * multiplier;
- multiplier = multiplier<<4;
- }
- return (hex_val);
- }
- /***********************************************************************************************
- * Search_For_Char -- search through ascii data for a particular character *
- * *
- * *
- * *
- * INPUT: character *
- * ptr to buffer *
- * length of buffer *
- * *
- * OUTPUT: ptr to char in buffer or NULL if not found *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:41PM ST : Created *
- *=============================================================================================*/
- char *Search_For_Char (char character , char buffer_ptr[] , int buffer_length)
- {
- for ( unsigned i=0 ; i<buffer_length ; i++){
- if (buffer_ptr[i]==character){
- return ((char*) (unsigned)buffer_ptr+i);
- }
- }
- return (NULL);
- }
- /***********************************************************************************************
- * Search_For_String -- search for a string of chars within a buffer *
- * *
- * *
- * *
- * INPUT: string *
- * ptr to buffer to search in *
- * length of buffer *
- * *
- * OUTPUT: ptr to string in buffer or NULL if not found *
- * *
- * WARNINGS: None *
- * *
- * HISTORY: *
- * 11/20/95 5:42PM ST : Created *
- *=============================================================================================*/
- char *Search_For_String (char *string , char *buffer_ptr , int buffer_length)
- {
- int j;
- int string_length=strlen(string);
- for (int i=0 ; i<buffer_length-string_length ; i++){
- for (j=0 ; j<string_length ; j++){
- if ( *(string+j) != *(buffer_ptr+i+j)) break;
- }
- if (j==string_length) return buffer_ptr+i;
- }
- return (NULL);
- }
|